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
*/
@HiltAndroidApp
public class ApplicationContext extends Application implements DefaultLifecycleObserver, ConfigFactoryUpdateListener, Toaster {
public class ApplicationContext extends Application implements DefaultLifecycleObserver, Toaster {
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
@ -214,15 +214,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return this.persistentLogger;
}
@Override
public void notifyUpdates(@NotNull Config forConfigObject, long messageTimestamp) {
// forward to the config factory / storage ig
if (forConfigObject instanceof UserProfile && !textSecurePreferences.getConfigurationMessageSynced()) {
textSecurePreferences.setConfigurationMessageSynced(true);
}
storage.notifyConfigUpdates(forConfigObject, messageTimestamp);
}
@Override
public void toast(@StringRes int stringRes, int toastLength, @NonNull Map<String, String> parameters) {
Phrase builder = Phrase.from(this, stringRes);
@ -510,7 +501,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
Log.d("Loki", "Failed to delete database.");
return false;
}
configFactory.keyPairChanged();
configFactory.clearAll();
return true;
}

View File

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

View File

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

View File

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

View File

@ -9,6 +9,8 @@ import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
typealias ConfigVariant = String
class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) {
companion object {
@ -25,12 +27,17 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co
private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?"
private const val VARIANT_IN_AND_PUBKEY_WHERE = "$VARIANT in (?) AND $PUBKEY = ?"
val KEYS_VARIANT = SharedConfigMessage.Kind.ENCRYPTION_KEYS.name
val INFO_VARIANT = SharedConfigMessage.Kind.CLOSED_GROUP_INFO.name
val MEMBER_VARIANT = SharedConfigMessage.Kind.CLOSED_GROUP_MEMBERS.name
val CONTACTS_VARIANT: ConfigVariant = SharedConfigMessage.Kind.CONTACTS.name
val USER_GROUPS_VARIANT: ConfigVariant = SharedConfigMessage.Kind.GROUPS.name
val USER_PROFILE_VARIANT: ConfigVariant = SharedConfigMessage.Kind.USER_PROFILE.name
val CONVO_INFO_VARIANT: ConfigVariant = SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name
val KEYS_VARIANT: ConfigVariant = SharedConfigMessage.Kind.ENCRYPTION_KEYS.name
val INFO_VARIANT: ConfigVariant = SharedConfigMessage.Kind.CLOSED_GROUP_INFO.name
val MEMBER_VARIANT: ConfigVariant = SharedConfigMessage.Kind.CLOSED_GROUP_MEMBERS.name
}
fun storeConfig(variant: String, publicKey: String, data: ByteArray, timestamp: Long) {
fun storeConfig(variant: ConfigVariant, publicKey: String, data: ByteArray, timestamp: Long) {
val db = writableDatabase
val contentValues = contentValuesOf(
VARIANT to variant,
@ -84,7 +91,7 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co
}
}
fun retrieveConfigAndHashes(variant: String, publicKey: String): ByteArray? {
fun retrieveConfigAndHashes(variant: ConfigVariant, publicKey: String): ByteArray? {
val db = readableDatabase
val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
return query?.use { cursor ->
@ -94,7 +101,7 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co
}
}
fun retrieveConfigLastUpdateTimestamp(variant: String, publicKey: String): Long {
fun retrieveConfigLastUpdateTimestamp(variant: ConfigVariant, publicKey: String): Long {
val db = readableDatabase
val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
if (cursor == null) return 0

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.BackgroundGroupAddJob
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
import org.session.libsession.messaging.jobs.InviteContactsJob
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.jobs.MessageSendJob
@ -79,13 +78,6 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return result.firstOrNull { job -> job.attachmentID == attachmentID }
}
fun getGroupInviteJob(groupSessionId: String, memberSessionId: String): InviteContactsJob? {
val database = databaseHelper.readableDatabase
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(InviteContactsJob.KEY)) { cursor ->
jobFromCursor(cursor) as? InviteContactsJob
}.firstOrNull { it != null && it.groupSessionId == groupSessionId && it.memberSessionIds.contains(memberSessionId) }
}
fun getMessageSendJob(messageSendJobID: String): MessageSendJob? {
val database = databaseHelper.readableDatabase
return database.get(sessionJobTable, "$jobID = ? AND $jobType = ?", arrayOf( messageSendJobID, MessageSendJob.KEY )) { cursor ->

View File

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

View File

@ -1,25 +1,17 @@
package org.thoughtcrime.securesms.debugmenu
import android.app.Application
import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Environment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.session.libsession.utilities.Environment
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject
@HiltViewModel
@ -75,11 +67,6 @@ class DebugMenuViewModel @Inject constructor(
// clear remote and local data, then restart the app
viewModelScope.launch {
try {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application)
} catch (e: Exception) {
// we can ignore fails here as we might be switching environments before the user gets a public key
}
ApplicationContext.getInstance(application).clearAllData().let { success ->
if(success){
// save the environment

View File

@ -1,40 +1,57 @@
package org.thoughtcrime.securesms.dependencies
import android.content.Context
import android.os.Trace
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import network.loki.messenger.libsession_util.Config
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.GroupInfoConfig
import network.loki.messenger.libsession_util.GroupKeysConfig
import network.loki.messenger.libsession_util.GroupMembersConfig
import network.loki.messenger.libsession_util.MutableContacts
import network.loki.messenger.libsession_util.MutableConversationVolatileConfig
import network.loki.messenger.libsession_util.MutableUserGroupsConfig
import network.loki.messenger.libsession_util.MutableUserProfile
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupInfo
import network.loki.messenger.libsession_util.util.Sodium
import org.session.libsession.messaging.messages.Destination
import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.snode.OwnedSwarmAuth
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SwarmAuth
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.ConfigFactoryUpdateListener
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsession.utilities.GroupConfigs
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MutableGroupConfigs
import org.session.libsession.utilities.MutableUserConfigs
import org.session.libsession.utilities.UserConfigs
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.ConfigDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import java.util.concurrent.ConcurrentHashMap
class ConfigFactory(
private val context: Context,
private val configDatabase: ConfigDatabase,
/** <ed25519 secret key,33 byte prefixed public key (hex encoded)> */
private val maybeGetUserInfo: () -> Pair<ByteArray, String>?
) :
ConfigFactoryProtocol {
private val threadDb: ThreadDatabase,
private val storage: StorageProtocol,
) : ConfigFactoryProtocol {
companion object {
// This is a buffer period within which we will process messages which would result in a
// config change, any message which would normally result in a config change which was sent
@ -43,351 +60,300 @@ class ConfigFactory(
const val configChangeBufferPeriod: Long = (2 * 60 * 1000)
}
fun keyPairChanged() { // this should only happen restoring or clearing datac
_userConfig?.free()
_contacts?.free()
_convoVolatileConfig?.free()
_userConfig = null
_contacts = null
_convoVolatileConfig = null
init {
System.loadLibrary("session_util")
}
private val userLock = Object()
private var _userConfig: UserProfile? = null
private val contactsLock = Object()
private var _contacts: Contacts? = null
private val convoVolatileLock = Object()
private var _convoVolatileConfig: ConversationVolatileConfig? = null
private val userGroupsLock = Object()
private var _userGroups: UserGroupsConfig? = null
private class UserConfigsImpl(
userEd25519SecKey: ByteArray,
private val userAccountId: AccountId,
private val configDatabase: ConfigDatabase,
storage: StorageProtocol,
threadDb: ThreadDatabase,
contactsDump: ByteArray? = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.CONTACTS_VARIANT,
userAccountId.hexString
),
userGroupsDump: ByteArray? = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.USER_GROUPS_VARIANT,
userAccountId.hexString
),
userProfileDump: ByteArray? = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.USER_PROFILE_VARIANT,
userAccountId.hexString
),
convoInfoDump: ByteArray? = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.CONVO_INFO_VARIANT,
userAccountId.hexString
)
) : MutableUserConfigs {
override val contacts = Contacts(
ed25519SecretKey = userEd25519SecKey,
initialDump = contactsDump,
)
private val isConfigForcedOn by lazy { TextSecurePreferences.hasForcedNewConfig(context) }
override val userGroups = UserGroupsConfig(
ed25519SecretKey = userEd25519SecKey,
initialDump = userGroupsDump
)
override val userProfile = UserProfile(
ed25519SecretKey = userEd25519SecKey,
initialDump = userProfileDump
)
override val convoInfoVolatile = ConversationVolatileConfig(
ed25519SecretKey = userEd25519SecKey,
initialDump = convoInfoDump,
)
private val listeners: MutableList<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,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
override val configUpdateNotifications get() = _configUpdateNotifications
fun registerListener(listener: ConfigFactoryUpdateListener) {
listeners += listener
}
private fun requiresCurrentUserAccountId(): AccountId =
AccountId(requireNotNull(storage.getUserPublicKey()) {
"No logged in user"
})
fun unregisterListener(listener: ConfigFactoryUpdateListener) {
listeners -= listener
}
private inline fun <T> synchronizedWithLog(lock: Any, body: () -> T): T {
Trace.beginSection("synchronizedWithLog")
val result = synchronized(lock) {
body()
private fun requiresCurrentUserED25519SecKey(): ByteArray =
requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) {
"No logged in user"
}
override fun <T> withUserConfigs(cb: (UserConfigs) -> T): T {
val userAccountId = requiresCurrentUserAccountId()
val configs = userConfigs.getOrPut(userAccountId) {
UserConfigsImpl(
requiresCurrentUserED25519SecKey(),
userAccountId,
threadDb = threadDb,
configDatabase = configDatabase,
storage = storage
)
}
return synchronized(configs) {
cb(configs)
}
Trace.endSection()
return result
}
override val user: UserProfile?
get() = synchronizedWithLog(userLock) {
if (_userConfig == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
val userDump = configDatabase.retrieveConfigAndHashes(
SharedConfigMessage.Kind.USER_PROFILE.name,
publicKey
)
_userConfig = if (userDump != null) {
UserProfile.newInstance(secretKey, userDump)
} else {
ConfigurationMessageUtilities.generateUserProfileConfigDump()?.let { dump ->
UserProfile.newInstance(secretKey, dump)
} ?: UserProfile.newInstance(secretKey)
}
override fun <T> withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T {
return withUserConfigs { configs ->
val result = cb(configs as UserConfigsImpl)
if (configs.persistIfDirty()) {
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.UserConfigs)
}
_userConfig
result
}
}
override fun <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 val contacts: Contacts?
get() = synchronizedWithLog(contactsLock) {
if (_contacts == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
val contactsDump = configDatabase.retrieveConfigAndHashes(
SharedConfigMessage.Kind.CONTACTS.name,
publicKey
)
_contacts = if (contactsDump != null) {
Contacts.newInstance(secretKey, contactsDump)
} else {
ConfigurationMessageUtilities.generateContactConfigDump()?.let { dump ->
Contacts.newInstance(secretKey, dump)
} ?: Contacts.newInstance(secretKey)
}
return synchronized(configs) {
cb(configs)
}
}
override fun <T> withMutableGroupConfigs(
groupId: AccountId,
cb: (MutableGroupConfigs) -> T
): T {
return withGroupConfigs(groupId) { configs ->
val result = cb(configs as GroupConfigsImpl)
if (configs.persistIfDirty()) {
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsUpdated(groupId))
}
_contacts
result
}
override val convoVolatile: ConversationVolatileConfig?
get() = synchronizedWithLog(convoVolatileLock) {
if (_convoVolatileConfig == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
val convoDump = configDatabase.retrieveConfigAndHashes(
SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name,
publicKey
)
_convoVolatileConfig = if (convoDump != null) {
ConversationVolatileConfig.newInstance(secretKey, convoDump)
} else {
ConfigurationMessageUtilities.generateConversationVolatileDump(context)
?.let { dump ->
ConversationVolatileConfig.newInstance(secretKey, dump)
} ?: ConversationVolatileConfig.newInstance(secretKey)
}
}
_convoVolatileConfig
}
override val userGroups: UserGroupsConfig?
get() = synchronizedWithLog(userGroupsLock) {
if (_userGroups == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
val userGroupsDump = configDatabase.retrieveConfigAndHashes(
SharedConfigMessage.Kind.GROUPS.name,
publicKey
)
_userGroups = if (userGroupsDump != null) {
UserGroupsConfig.Companion.newInstance(secretKey, userGroupsDump)
} else {
ConfigurationMessageUtilities.generateUserGroupDump(context)?.let { dump ->
UserGroupsConfig.Companion.newInstance(secretKey, dump)
} ?: UserGroupsConfig.newInstance(secretKey)
}
}
_userGroups
}
private fun getGroupInfo(groupSessionId: AccountId) = userGroups?.getClosedGroup(groupSessionId.hexString)
override fun getGroupInfoConfig(groupSessionId: AccountId): GroupInfoConfig? = getGroupInfo(groupSessionId)?.let { groupInfo ->
// get any potential initial dumps
val dump = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.INFO_VARIANT,
groupSessionId.hexString
) ?: byteArrayOf()
GroupInfoConfig.newInstance(groupSessionId.pubKeyBytes, groupInfo.adminKey, dump)
}
override fun getGroupKeysConfig(groupSessionId: AccountId,
info: GroupInfoConfig?,
members: GroupMembersConfig?,
free: Boolean): GroupKeysConfig? = getGroupInfo(groupSessionId)?.let { groupInfo ->
// Get the user info or return early
val (userSk, _) = maybeGetUserInfo() ?: return@let null
// Get the group info or return early
val usedInfo = info ?: getGroupInfoConfig(groupSessionId) ?: return@let null
// Get the group members or return early
val usedMembers = members ?: getGroupMemberConfig(groupSessionId) ?: return@let null
// Get the dump or empty
val dump = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.KEYS_VARIANT,
groupSessionId.hexString
) ?: byteArrayOf()
// Put it all together
val keys = GroupKeysConfig.newInstance(
userSk,
groupSessionId.pubKeyBytes,
groupInfo.adminKey,
dump,
usedInfo,
usedMembers
)
if (free) {
info?.free()
members?.free()
override fun removeGroup(groupId: AccountId) {
withMutableUserConfigs {
it.userGroups.eraseClosedGroup(groupId.hexString)
}
if (usedInfo !== info) usedInfo.free()
if (usedMembers !== members) usedMembers.free()
keys
}
override fun getGroupMemberConfig(groupSessionId: AccountId): GroupMembersConfig? = getGroupInfo(groupSessionId)?.let { groupInfo ->
// Get initial dump if we have one
val dump = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.MEMBER_VARIANT,
groupSessionId.hexString
) ?: byteArrayOf()
GroupMembersConfig.newInstance(
groupSessionId.pubKeyBytes,
groupInfo.adminKey,
dump
)
}
override fun constructGroupKeysConfig(
groupSessionId: AccountId,
info: GroupInfoConfig,
members: GroupMembersConfig
): GroupKeysConfig? = getGroupInfo(groupSessionId)?.let { groupInfo ->
val (userSk, _) = maybeGetUserInfo() ?: return null
GroupKeysConfig.newInstance(
userSk,
groupSessionId.pubKeyBytes,
groupInfo.adminKey,
info = info,
members = members
)
}
override fun userSessionId(): AccountId? {
return maybeGetUserInfo()?.second?.let(::AccountId)
}
override fun maybeDecryptForUser(encoded: ByteArray, domain: String, closedGroupSessionId: AccountId): ByteArray? {
val secret = maybeGetUserInfo()?.first ?: run {
Log.e("ConfigFactory", "No user ed25519 secret key decrypting a message for us")
return null
if (groupConfigs.remove(groupId) != null) {
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsDeleted(groupId))
}
configDatabase.deleteGroupConfigs(groupId)
}
override fun maybeDecryptForUser(
encoded: ByteArray,
domain: String,
closedGroupSessionId: AccountId
): ByteArray? {
return Sodium.decryptForMultipleSimple(
encoded = encoded,
ed25519SecretKey = secret,
ed25519SecretKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) {
"No logged in user"
},
domain = domain,
senderPubKey = Sodium.ed25519PkToCurve25519(closedGroupSessionId.pubKeyBytes)
)
}
override fun getUserConfigs(): List<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(
publicKey: String?,
groupPublicKey: String?,
openGroupId: String?,
visibleOnly: Boolean
): Boolean {
val (_, userPublicKey) = maybeGetUserInfo() ?: return true
val userPublicKey = storage.getUserPublicKey() ?: return false
if (openGroupId != null) {
val userGroups = userGroups ?: return false
val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context)
val openGroup =
get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false
// Not handling the `hidden` behaviour for communities so just indicate the existence
return (userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null)
return withUserConfigs {
it.userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null
}
} else if (groupPublicKey != null) {
val userGroups = userGroups ?: return false
// Not handling the `hidden` behaviour for legacy groups so just indicate the existence
return if (groupPublicKey.startsWith(IdPrefix.GROUP.value)) {
userGroups.getClosedGroup(groupPublicKey) != null
} else {
userGroups.getLegacyGroupInfo(groupPublicKey) != null
return withUserConfigs {
if (groupPublicKey.startsWith(IdPrefix.GROUP.value)) {
it.userGroups.getClosedGroup(groupPublicKey) != null
} else {
it.userGroups.getLegacyGroupInfo(groupPublicKey) != null
}
}
} else if (publicKey == userPublicKey) {
val user = user ?: return false
return (!visibleOnly || user.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN)
return withUserConfigs {
!visibleOnly || it.userProfile.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN
}
} else if (publicKey != null) {
val contacts = contacts ?: return false
val targetContact = contacts.get(publicKey) ?: return false
return (!visibleOnly || targetContact.priority != ConfigBase.PRIORITY_HIDDEN)
return withUserConfigs {
(!visibleOnly || it.contacts.get(publicKey)?.priority != ConfigBase.PRIORITY_HIDDEN)
}
} else {
return false
}
return false
}
override fun canPerformChange(
@ -402,32 +368,192 @@ class ConfigFactory(
return (changeTimestampMs >= (lastUpdateTimestampMs - configChangeBufferPeriod))
}
override fun saveGroupConfigs(
groupKeys: GroupKeysConfig,
groupInfo: GroupInfoConfig,
groupMembers: GroupMembersConfig
) {
val pubKey = groupInfo.id().hexString
val timestamp = SnodeAPI.nowWithOffset
override fun getGroupAuth(groupId: AccountId): SwarmAuth? {
val (adminKey, authData) = withUserConfigs {
val group = it.userGroups.getClosedGroup(groupId.hexString)
group?.adminKey to group?.authData
}
// this would be nicer with a .any iteration or something but the base types don't line up
val anyNeedDump = groupKeys.needsDump() || groupInfo.needsDump() || groupMembers.needsDump()
if (!anyNeedDump) return Log.d("ConfigFactory", "Group config doesn't need dump, skipping")
else Log.d("ConfigFactory", "Group config needs dump, storing and notifying")
configDatabase.storeGroupConfigs(pubKey, groupKeys.dump(), groupInfo.dump(), groupMembers.dump(), timestamp)
_configUpdateNotifications.tryEmit(Unit)
return if (adminKey != null) {
OwnedSwarmAuth.ofClosedGroup(groupId, adminKey)
} else if (authData != null) {
GroupSubAccountSwarmAuth(groupId, this, authData)
} else {
null
}
}
override fun removeGroup(closedGroupId: AccountId) {
val groups = userGroups ?: return
groups.eraseClosedGroup(closedGroupId.hexString)
persist(groups, SnodeAPI.nowWithOffset)
configDatabase.deleteGroupConfigs(closedGroupId)
fun clearAll() {
//TODO: clear all configsr
}
override fun scheduleUpdate(destination: Destination) {
// there's probably a better way to do this
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(destination)
private class GroupSubAccountSwarmAuth(
override val accountId: AccountId,
val factory: ConfigFactory,
val authData: ByteArray,
) : SwarmAuth {
override val ed25519PublicKeyHex: String?
get() = null
override fun sign(data: ByteArray): Map<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 kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.plus
import network.loki.messenger.libsession_util.util.GroupInfo
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller
import org.session.libsignal.utilities.AccountId
@ -16,23 +15,29 @@ class PollerFactory(
private val executor: CoroutineDispatcher,
private val configFactory: ConfigFactory,
private val groupManagerV2: Lazy<GroupManagerV2>,
private val storage: StorageProtocol,
) {
private val pollers = ConcurrentHashMap<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
if (configFactory.userGroups?.getClosedGroup(sessionId.hexString)?.invited != false) return null
val invited = configFactory.withUserConfigs {
it.userGroups.getClosedGroup(sessionId.hexString)?.invited
}
if (invited != false) return null
return pollers.getOrPut(sessionId) {
ClosedGroupPoller(scope + SupervisorJob(), executor, sessionId, configFactory, groupManagerV2.get())
ClosedGroupPoller(scope, executor, sessionId, configFactory, groupManagerV2.get(), storage)
}
}
fun startAll() {
configFactory.userGroups?.allClosedGroupInfo()?.filterNot(GroupInfo.ClosedGroupInfo::invited)?.forEach {
pollerFor(it.groupAccountId)?.start()
}
configFactory
.withUserConfigs { it.userGroups.allClosedGroupInfo() }
.filterNot(GroupInfo.ClosedGroupInfo::invited)
.forEach { pollerFor(it.groupAccountId)?.start() }
}
fun stopAll() {
@ -42,7 +47,8 @@ class PollerFactory(
}
fun updatePollers() {
val currentGroups = configFactory.userGroups?.allClosedGroupInfo()?.filterNot(GroupInfo.ClosedGroupInfo::invited) ?: return
val currentGroups = configFactory
.withUserConfigs { it.userGroups.allClosedGroupInfo() }.filterNot(GroupInfo.ClosedGroupInfo::invited)
val toRemove = pollers.filter { (id, _) -> id !in currentGroups.map { it.groupAccountId } }
toRemove.forEach { (id, _) ->
pollers.remove(id)?.stop()

View File

@ -12,40 +12,32 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.utilities.ConfigFactoryUpdateListener
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.database.ConfigDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import javax.inject.Named
import javax.inject.Singleton
@Suppress("OPT_IN_USAGE")
@Module
@InstallIn(SingletonComponent::class)
object SessionUtilModule {
const val POLLER_SCOPE = "poller_coroutine_scope"
private fun maybeUserEdSecretKey(context: Context): ByteArray? {
val edKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return null
return edKey.secretKey.asBytes
}
private const val POLLER_SCOPE = "poller_coroutine_scope"
@Provides
@Singleton
fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory =
ConfigFactory(context, configDatabase) {
val localUserPublicKey = TextSecurePreferences.getLocalNumber(context)
val secretKey = maybeUserEdSecretKey(context)
if (localUserPublicKey == null || secretKey == null) null
else secretKey to localUserPublicKey
}.apply {
registerListener(context as ConfigFactoryUpdateListener)
}
fun provideConfigFactory(
@ApplicationContext context: Context,
configDatabase: ConfigDatabase,
storageProtocol: StorageProtocol,
threadDatabase: ThreadDatabase,
): ConfigFactory = ConfigFactory(context, configDatabase, threadDatabase, storageProtocol)
@Provides
@Named(POLLER_SCOPE)
fun providePollerScope(@ApplicationContext applicationContext: Context): CoroutineScope = GlobalScope
fun providePollerScope(): CoroutineScope = GlobalScope
@OptIn(ExperimentalCoroutinesApi::class)
@Provides
@ -57,6 +49,12 @@ object SessionUtilModule {
fun providePollerFactory(@Named(POLLER_SCOPE) coroutineScope: CoroutineScope,
@Named(POLLER_SCOPE) dispatcher: CoroutineDispatcher,
configFactory: ConfigFactory,
groupManagerV2: Lazy<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,22 +34,25 @@ object ClosedGroupManager {
}
fun ConfigFactory.updateLegacyGroup(group: GroupRecord) {
val groups = userGroups ?: return
if (!group.isLegacyClosedGroup) return
val storage = MessagingModuleConfiguration.shared.storage
val threadId = storage.getThreadId(group.encodedId) ?: return
val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId())
val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey)
val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize))
val toSet = legacyInfo.copy(
members = latestMemberMap,
name = group.title,
priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = latestKeyPair.privateKey.serialize()
)
groups.set(toSet)
}
withMutableUserConfigs {
val groups = it.userGroups
val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey)
val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize))
val toSet = legacyInfo.copy(
members = latestMemberMap,
name = group.title,
priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = latestKeyPair.privateKey.serialize()
)
groups.set(toSet)
}
}
}

View File

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

View File

@ -85,8 +85,7 @@ class GroupManagerV2Impl @Inject constructor(
*/
private fun requireAdminAccess(group: AccountId): ByteArray {
return checkNotNull(configFactory
.userGroups
?.getClosedGroup(group.hexString)
.withUserConfigs { it.userGroups.getClosedGroup(group.hexString) }
?.adminKey
?.takeIf { it.isNotEmpty() }) { "Only admin is allowed to invite members" }
}
@ -96,10 +95,6 @@ class GroupManagerV2Impl @Inject constructor(
groupDescription: String,
members: Set<Contact>
): Recipient = withContext(dispatcher) {
val userGroupsConfig =
requireNotNull(configFactory.userGroups) { "User groups config is not available" }
val convoVolatileConfig =
requireNotNull(configFactory.convoVolatile) { "Conversation volatile config is not available" }
val ourAccountId =
requireNotNull(storage.getUserPublicKey()) { "Our account ID is not available" }
val ourKeys =
@ -109,25 +104,23 @@ class GroupManagerV2Impl @Inject constructor(
val groupCreationTimestamp = SnodeAPI.nowWithOffset
// Create a group in the user groups config
val group = userGroupsConfig.createGroup()
val group = configFactory.withMutableUserConfigs { configs ->
configs.userGroups.createGroup().also(configs.userGroups::set)
}
val adminKey = checkNotNull(group.adminKey) { "Admin key is null for new group creation." }
userGroupsConfig.set(group)
val groupId = group.groupAccountId
val groupAuth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey)
try {
withNewGroupConfigs(
groupId = groupId,
userSecretKey = ourKeys.secretKey.asBytes,
groupAdminKey = adminKey
) { infoConfig, membersConfig, keysConfig ->
configFactory.withMutableGroupConfigs(groupId) { configs ->
// Update group's information
infoConfig.setName(groupName)
infoConfig.setDescription(groupDescription)
configs.groupInfo.setName(groupName)
configs.groupInfo.setDescription(groupDescription)
// Add members
for (member in members) {
membersConfig.set(
configs.groupMembers.set(
GroupMember(
sessionId = member.accountID,
name = member.name,
@ -138,7 +131,7 @@ class GroupManagerV2Impl @Inject constructor(
}
// Add ourselves as admin
membersConfig.set(
configs.groupMembers.set(
GroupMember(
sessionId = ourAccountId,
name = ourProfile.displayName,
@ -148,151 +141,48 @@ class GroupManagerV2Impl @Inject constructor(
)
// Manually re-key to prevent issue with linked admin devices
keysConfig.rekey(infoConfig, membersConfig)
configs.rekeys()
}
val configTtl = 14 * 24 * 60 * 60 * 1000L // 14 days
// Push keys
val pendingKey = requireNotNull(keysConfig.pendingConfig()) {
"Expect pending keys data to push but got none"
}
val pushKeys = async {
SnodeAPI.sendBatchRequest(
groupId,
SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = keysConfig.namespace(),
message = SnodeMessage(
recipient = groupId.hexString,
data = Base64.encodeBytes(pendingKey),
ttl = configTtl,
timestamp = groupCreationTimestamp
),
auth = groupAuth
),
StoreMessageResponse::class.java
)
}
// Push info
val pushInfo = async {
val (infoPush, infoSeqNo) = infoConfig.push()
infoSeqNo to SnodeAPI.sendBatchRequest(
groupId,
SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = infoConfig.namespace(),
message = SnodeMessage(
recipient = groupId.hexString,
data = Base64.encodeBytes(infoPush),
ttl = configTtl,
timestamp = groupCreationTimestamp
),
auth = groupAuth
),
StoreMessageResponse::class.java
)
}
// Members push
val pushMembers = async {
val (membersPush, membersSeqNo) = membersConfig.push()
membersSeqNo to SnodeAPI.sendBatchRequest(
groupId,
SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = membersConfig.namespace(),
message = SnodeMessage(
recipient = groupId.hexString,
data = Base64.encodeBytes(membersPush),
ttl = configTtl,
timestamp = groupCreationTimestamp
),
auth = groupAuth
),
StoreMessageResponse::class.java
)
}
// Wait for all the push requests to finish then update the configs
val (keyHash, keyTimestamp) = pushKeys.await()
val (infoSeqNo, infoHash) = pushInfo.await()
val (membersSeqNo, membersHash) = pushMembers.await()
keysConfig.loadKey(pendingKey, keyHash, keyTimestamp, infoConfig, membersConfig)
infoConfig.confirmPushed(infoSeqNo, infoHash.hash)
membersConfig.confirmPushed(membersSeqNo, membersHash.hash)
configFactory.saveGroupConfigs(keysConfig, infoConfig, membersConfig)
// Add a new conversation into the volatile convo config and sync
convoVolatileConfig.set(
configFactory.withMutableUserConfigs {
it.convoInfoVolatile.set(
Conversation.ClosedGroup(
groupId.hexString,
groupCreationTimestamp,
false
)
)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application)
val recipient =
Recipient.from(application, Address.fromSerialized(groupId.hexString), false)
// Apply various data locally
profileManager.setName(application, recipient, groupName)
storage.setRecipientApprovedMe(recipient, true)
storage.setRecipientApproved(recipient, true)
pollerFactory.updatePollers()
// Invite members
JobQueue.shared.add(
InviteContactsJob(
groupSessionId = groupId.hexString,
memberSessionIds = members.map { it.accountID }.toTypedArray()
)
)
recipient
}
val recipient =
Recipient.from(application, Address.fromSerialized(groupId.hexString), false)
// Apply various data locally
profileManager.setName(application, recipient, groupName)
storage.setRecipientApprovedMe(recipient, true)
storage.setRecipientApproved(recipient, true)
pollerFactory.updatePollers()
// Invite members
JobQueue.shared.add(
InviteContactsJob(
groupSessionId = groupId.hexString,
memberSessionIds = members.map { it.accountID }.toTypedArray()
)
)
recipient
} catch (e: Exception) {
Log.e(TAG, "Failed to create group", e)
// Remove the group from the user groups config is sufficient as a "rollback"
userGroupsConfig.erase(group)
configFactory.withMutableUserConfigs {
it.userGroups.eraseClosedGroup(groupId.hexString)
}
throw e
}
}
private suspend fun <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(
group: AccountId,

View File

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

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

View File

@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.getConversationUnread
@ -119,7 +118,8 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
binding.leaveTextView.isVisible = recipient.isGroupRecipient && isCurrentUserInGroup
binding.leaveTextView.setOnClickListener(this)
binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 || configFactory.convoVolatile?.getConversationUnread(thread) == true
binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 ||
configFactory.withUserConfigs { it.convoInfoVolatile.getConversationUnread(thread) }
binding.markAllAsReadTextView.setOnClickListener(this)
binding.pinTextView.isVisible = !thread.isPinned
binding.unpinTextView.isVisible = thread.isPinned

View File

@ -97,7 +97,7 @@ class ConversationView : LinearLayout {
val textSize = if (unreadCount < 1000) 12.0f else 10.0f
binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead)
|| (configFactory.convoVolatile?.getConversationUnread(thread) == true)
|| (configFactory.withUserConfigs { it.convoInfoVolatile.getConversationUnread(thread) })
binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup)
val senderDisplayName = getTitle(thread.recipient)

View File

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

View File

@ -46,7 +46,7 @@ class HomeDiffUtil(
oldItem.isSent == newItem.isSent &&
oldItem.isPending == newItem.isPending &&
oldItem.lastSeen == newItem.lastSeen &&
configFactory.convoVolatile?.getConversationUnread(newItem) != true &&
!configFactory.withUserConfigs { it.convoInfoVolatile.getConversationUnread(newItem) } &&
old.typingThreadIDs.contains(oldItem.threadId) == new.typingThreadIDs.contains(newItem.threadId)
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@ import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
@ -24,7 +23,6 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject
@ -124,15 +122,6 @@ class ClearAllDataDialog : DialogFragment() {
}
private suspend fun performDeleteLocalDataOnlyStep() {
try {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext())
} catch (e: Exception) {
Log.e(TAG, "Failed to force sync when deleting data", e)
withContext(Main) {
Toast.makeText(ApplicationContext.getInstance(requireContext()), R.string.errorUnknown, Toast.LENGTH_LONG).show()
}
return
}
ApplicationContext.getInstance(context).clearAllDataAndRestart().let { success ->
withContext(Main) {
if (success) {

View File

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

View File

@ -295,16 +295,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} else {
// if we have a network connection then attempt to update the display name
TextSecurePreferences.setProfileName(this, displayName)
val user = viewModel.getUser()
if (user == null) {
Log.w(TAG, "Cannot update display name - missing user details from configFactory.")
} else {
user.setName(displayName)
// sync remote config
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this)
binding.btnGroupNameDisplay.text = displayName
updateWasSuccessful = true
}
viewModel.updateName(displayName)
binding.btnGroupNameDisplay.text = displayName
updateWasSuccessful = true
}
// Inform the user if we failed to update the display name

View File

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

View File

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

View File

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

View File

@ -1,254 +1,10 @@
package org.thoughtcrime.securesms.util
import android.content.Context
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupInfo
import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.ConfigurationSyncJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.WindowDebouncer
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import java.util.Timer
import java.util.concurrent.ConcurrentLinkedDeque
object ConfigurationMessageUtilities {
private const val TAG = "ConfigMessageUtils"
private val debouncer = WindowDebouncer(3000, Timer())
private val destinationUpdater = Any()
private val pendingDestinations = ConcurrentLinkedDeque<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
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}%');

View File

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

View File

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

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;
});
}
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"
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) {

View File

@ -1,41 +1,6 @@
#include <jni.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"
JNIEXPORT jint JNICALL
@ -46,7 +11,6 @@ Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeOneT
return conversations->size_1to1();
}
#pragma clang diagnostic pop
extern "C"
JNIEXPORT jint JNICALL
Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseAll(JNIEnv *env,

View File

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

View File

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

View File

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

View File

@ -1,44 +1,6 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "bugprone-reserved-identifier"
#include "user_groups.h"
#include "oxenc/hex.h"
#pragma clang diagnostic push
#pragma ide diagnostic ignored "bugprone-reserved-identifier"
extern "C"
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_UserGroupsConfig_00024Companion_newInstance___3B(
JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key) {
std::lock_guard lock{util::util_mutex_};
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
auto* user_groups = new session::config::UserGroups(secret_key, std::nullopt);
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig");
jmethodID constructor = env->GetMethodID(contactsClass, "<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"
JNIEXPORT jint JNICALL
Java_network_loki_messenger_libsession_1util_util_GroupInfo_00024LegacyGroupInfo_00024Companion_NAME_1MAX_1LENGTH(

View File

@ -2,39 +2,6 @@
#include "util.h"
extern "C" {
#pragma clang diagnostic push
#pragma ide diagnostic ignored "bugprone-reserved-identifier"
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_00024Companion_newInstance___3B_3B(
JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
std::lock_guard lock{util::util_mutex_};
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
auto initial = util::ustring_from_bytes(env, initial_dump);
auto* profile = new session::config::UserProfile(secret_key, std::optional(initial));
jclass userClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile");
jmethodID constructor = env->GetMethodID(userClass, "<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
Java_network_loki_messenger_libsession_1util_UserProfile_setName(
JNIEnv* env,

View File

@ -16,15 +16,43 @@ import org.session.libsignal.utilities.Namespace
import java.io.Closeable
import java.util.Stack
sealed class Config(protected val pointer: Long): Closeable {
sealed class Config(initialPointer: Long): Closeable {
var pointer = initialPointer
private set
init {
check(pointer != 0L) { "Pointer is null" }
}
abstract fun namespace(): Int
external fun free()
override fun close() {
free()
private external fun free()
final override fun close() {
if (pointer != 0L) {
free()
pointer = 0L
}
}
}
sealed class ConfigBase(pointer: Long): Config(pointer) {
interface ReadableConfig {
fun namespace(): Int
fun needsPush(): Boolean
fun needsDump(): Boolean
fun currentHashes(): List<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 {
init {
System.loadLibrary("session_util")
@ -46,42 +74,35 @@ sealed class ConfigBase(pointer: Long): Config(pointer) {
}
external fun dirty(): Boolean
external fun needsPush(): Boolean
external fun needsDump(): Boolean
external fun push(): ConfigPush
external fun dump(): ByteArray
external fun encryptionDomain(): String
external fun confirmPushed(seqNo: Long, newHash: String)
external fun merge(toMerge: Array<Pair<String,ByteArray>>): Stack<String>
external fun currentHashes(): List<String>
external override fun dirty(): Boolean
external override fun needsPush(): Boolean
external override fun needsDump(): Boolean
external override fun push(): ConfigPush
external override fun dump(): ByteArray
external override fun encryptionDomain(): String
external override fun confirmPushed(seqNo: Long, newHash: String)
external override fun merge(toMerge: Array<Pair<String,ByteArray>>): Stack<String>
external override fun currentHashes(): List<String>
// Singular merge
external fun merge(toMerge: Pair<String,ByteArray>): Stack<String>
}
class Contacts(pointer: Long) : ConfigBase(pointer) {
companion object {
init {
System.loadLibrary("session_util")
}
external fun newInstance(ed25519SecretKey: ByteArray): Contacts
external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): Contacts
}
override fun namespace() = Namespace.CONTACTS()
interface ReadableContacts: ReadableConfig {
fun get(accountId: String): Contact?
fun all(): List<Contact>
}
external fun get(accountId: String): Contact?
external fun getOrConstruct(accountId: String): Contact
external fun all(): List<Contact>
external fun set(contact: Contact)
external fun erase(accountId: String): Boolean
interface MutableContacts : ReadableContacts, MutableConfig {
fun getOrConstruct(accountId: String): Contact
fun set(contact: Contact)
fun erase(accountId: String): Boolean
/**
* Similar to [updateIfExists], but will create the underlying contact if it doesn't exist before passing to [updateFunction]
*/
fun upsertContact(accountId: String, updateFunction: Contact.()->Unit = {}) {
fun upsertContact(accountId: String, updateFunction: Contact.() -> Unit = {}) {
when {
accountId.startsWith(IdPrefix.BLINDED.value) -> Log.w("Loki", "Trying to create a contact with a blinded ID prefix")
accountId.startsWith(IdPrefix.UN_BLINDED.value) -> Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix")
@ -92,251 +113,403 @@ class Contacts(pointer: Long) : ConfigBase(pointer) {
}
}
}
/**
* Updates the contact by accountId with a given [updateFunction], and applies to the underlying config.
* the [updateFunction] doesn't run if there is no contact
*/
private fun updateIfExists(accountId: String, updateFunction: Contact.()->Unit) {
when {
accountId.startsWith(IdPrefix.BLINDED.value) -> Log.w("Loki", "Trying to create a contact with a blinded ID prefix")
accountId.startsWith(IdPrefix.UN_BLINDED.value) -> Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix")
accountId.startsWith(IdPrefix.BLINDEDV2.value) -> Log.w("Loki", "Trying to create a contact with a blindedv2 ID prefix")
else -> get(accountId)?.let {
updateFunction(it)
set(it)
}
}
}
}
class UserProfile(pointer: Long) : ConfigBase(pointer) {
companion object {
init {
System.loadLibrary("session_util")
}
external fun newInstance(ed25519SecretKey: ByteArray): UserProfile
external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserProfile
}
class Contacts private constructor(pointer: Long) : ConfigBase(pointer), MutableContacts {
constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this(
createConfigObject(
"Contacts",
ed25519SecretKey,
initialDump
)
)
override fun namespace() = Namespace.CONTACTS()
external override fun get(accountId: String): Contact?
external override fun getOrConstruct(accountId: String): Contact
external override fun all(): List<Contact>
external override fun set(contact: Contact)
external override fun erase(accountId: String): Boolean
}
interface ReadableUserProfile: ReadableConfig {
fun getName(): String?
fun getPic(): UserPic
fun getNtsPriority(): Long
fun getNtsExpiry(): ExpiryMode
fun getCommunityMessageRequests(): Boolean
fun isBlockCommunityMessageRequestsSet(): Boolean
}
interface MutableUserProfile : ReadableUserProfile, MutableConfig {
fun setName(newName: String)
fun setPic(userPic: UserPic)
fun setNtsPriority(priority: Long)
fun setNtsExpiry(expiryMode: ExpiryMode)
fun setCommunityMessageRequests(blocks: Boolean)
}
class UserProfile private constructor(pointer: Long) : ConfigBase(pointer), MutableUserProfile {
constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this(
createConfigObject(
"UserProfile",
ed25519SecretKey,
initialDump
)
)
override fun namespace() = Namespace.USER_PROFILE()
external fun setName(newName: String)
external fun getName(): String?
external fun getPic(): UserPic
external fun setPic(userPic: UserPic)
external fun setNtsPriority(priority: Long)
external fun getNtsPriority(): Long
external fun setNtsExpiry(expiryMode: ExpiryMode)
external fun getNtsExpiry(): ExpiryMode
external fun getCommunityMessageRequests(): Boolean
external fun setCommunityMessageRequests(blocks: Boolean)
external fun isBlockCommunityMessageRequestsSet(): Boolean
external override fun setName(newName: String)
external override fun getName(): String?
external override fun getPic(): UserPic
external override fun setPic(userPic: UserPic)
external override fun setNtsPriority(priority: Long)
external override fun getNtsPriority(): Long
external override fun setNtsExpiry(expiryMode: ExpiryMode)
external override fun getNtsExpiry(): ExpiryMode
external override fun getCommunityMessageRequests(): Boolean
external override fun setCommunityMessageRequests(blocks: Boolean)
external override fun isBlockCommunityMessageRequestsSet(): Boolean
}
class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) {
companion object {
init {
System.loadLibrary("session_util")
}
external fun newInstance(ed25519SecretKey: ByteArray): ConversationVolatileConfig
external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): ConversationVolatileConfig
}
interface ReadableConversationVolatileConfig: ReadableConfig {
fun getOneToOne(pubKeyHex: String): Conversation.OneToOne?
fun getCommunity(baseUrl: String, room: String): Conversation.Community?
fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup?
fun getClosedGroup(sessionId: String): Conversation.ClosedGroup?
fun sizeOneToOnes(): Int
fun sizeCommunities(): Int
fun sizeLegacyClosedGroups(): Int
fun size(): Int
fun empty(): Boolean
fun allOneToOnes(): List<Conversation.OneToOne>
fun allCommunities(): List<Conversation.Community>
fun allLegacyClosedGroups(): List<Conversation.LegacyGroup>
fun allClosedGroups(): List<Conversation.ClosedGroup>
fun all(): List<Conversation?>
}
interface MutableConversationVolatileConfig : ReadableConversationVolatileConfig, MutableConfig {
fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne
fun eraseOneToOne(pubKeyHex: String): Boolean
fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community
fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community
fun eraseCommunity(community: Conversation.Community): Boolean
fun eraseCommunity(baseUrl: String, room: String): Boolean
fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup
fun eraseLegacyClosedGroup(groupId: String): Boolean
fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup
fun eraseClosedGroup(sessionId: String): Boolean
fun erase(conversation: Conversation): Boolean
fun set(toStore: Conversation)
fun eraseAll(predicate: (Conversation) -> Boolean): Int
}
class ConversationVolatileConfig private constructor(pointer: Long): ConfigBase(pointer), MutableConversationVolatileConfig {
constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this(
createConfigObject(
"ConvoInfoVolatile",
ed25519SecretKey,
initialDump
)
)
override fun namespace() = Namespace.CONVO_INFO_VOLATILE()
external fun getOneToOne(pubKeyHex: String): Conversation.OneToOne?
external fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne
external fun eraseOneToOne(pubKeyHex: String): Boolean
external override fun getOneToOne(pubKeyHex: String): Conversation.OneToOne?
external override fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne
external override fun eraseOneToOne(pubKeyHex: String): Boolean
external fun getCommunity(baseUrl: String, room: String): Conversation.Community?
external fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community
external fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community
external fun eraseCommunity(community: Conversation.Community): Boolean
external fun eraseCommunity(baseUrl: String, room: String): Boolean
external override fun getCommunity(baseUrl: String, room: String): Conversation.Community?
external override fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community
external override fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community
external override fun eraseCommunity(community: Conversation.Community): Boolean
external override fun eraseCommunity(baseUrl: String, room: String): Boolean
external fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup?
external fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup
external fun eraseLegacyClosedGroup(groupId: String): Boolean
external override fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup?
external override fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup
external override fun eraseLegacyClosedGroup(groupId: String): Boolean
external fun getClosedGroup(sessionId: String): Conversation.ClosedGroup?
external fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup
external fun eraseClosedGroup(sessionId: String): Boolean
external override fun getClosedGroup(sessionId: String): Conversation.ClosedGroup?
external override fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup
external override fun eraseClosedGroup(sessionId: String): Boolean
external fun erase(conversation: Conversation): Boolean
external fun set(toStore: Conversation)
external override fun erase(conversation: Conversation): Boolean
external override fun set(toStore: Conversation)
/**
* Erase all conversations that do not satisfy the `predicate`, similar to [MutableList.removeAll]
*/
external fun eraseAll(predicate: (Conversation) -> Boolean): Int
external override fun eraseAll(predicate: (Conversation) -> Boolean): Int
external fun sizeOneToOnes(): Int
external fun sizeCommunities(): Int
external fun sizeLegacyClosedGroups(): Int
external fun size(): Int
external override fun sizeOneToOnes(): Int
external override fun sizeCommunities(): Int
external override fun sizeLegacyClosedGroups(): Int
external override fun size(): Int
external fun empty(): Boolean
external fun allOneToOnes(): List<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 empty(): Boolean
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) {
companion object {
init {
System.loadLibrary("session_util")
}
external fun newInstance(ed25519SecretKey: ByteArray): UserGroupsConfig
external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserGroupsConfig
}
interface ReadableUserGroupsConfig : ReadableConfig {
fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo?
fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo?
fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo?
fun sizeCommunityInfo(): Long
fun sizeLegacyGroupInfo(): Long
fun sizeClosedGroup(): Long
fun size(): Long
fun all(): List<GroupInfo>
fun allCommunityInfo(): List<GroupInfo.CommunityGroupInfo>
fun allLegacyGroupInfo(): List<GroupInfo.LegacyGroupInfo>
fun allClosedGroupInfo(): List<GroupInfo.ClosedGroupInfo>
}
interface MutableUserGroupsConfig : ReadableUserGroupsConfig, MutableConfig {
fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo
fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo
fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo
fun set(groupInfo: GroupInfo)
fun erase(groupInfo: GroupInfo)
fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean
fun eraseCommunity(server: String, room: String): Boolean
fun eraseLegacyGroup(accountId: String): Boolean
fun eraseClosedGroup(accountId: String): Boolean
fun createGroup(): GroupInfo.ClosedGroupInfo
}
class UserGroupsConfig private constructor(pointer: Long): ConfigBase(pointer), MutableUserGroupsConfig {
constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this(
createConfigObject(
"UserGroups",
ed25519SecretKey,
initialDump
)
)
override fun namespace() = Namespace.GROUPS()
external fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo?
external fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo?
external fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo?
external fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo
external fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo
external fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo
external fun set(groupInfo: GroupInfo)
external fun erase(groupInfo: GroupInfo)
external fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean
external fun eraseCommunity(server: String, room: String): Boolean
external fun eraseLegacyGroup(accountId: String): Boolean
external fun eraseClosedGroup(accountId: String): Boolean
external fun sizeCommunityInfo(): Long
external fun sizeLegacyGroupInfo(): Long
external fun sizeClosedGroup(): Long
external fun size(): Long
external fun all(): List<GroupInfo>
external fun allCommunityInfo(): List<GroupInfo.CommunityGroupInfo>
external fun allLegacyGroupInfo(): List<GroupInfo.LegacyGroupInfo>
external fun allClosedGroupInfo(): List<GroupInfo.ClosedGroupInfo>
external fun createGroup(): GroupInfo.ClosedGroupInfo
external override fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo?
external override fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo?
external override fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo?
external override fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo
external override fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo
external override fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo
external override fun set(groupInfo: GroupInfo)
external override fun erase(groupInfo: GroupInfo)
external override fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean
external override fun eraseCommunity(server: String, room: String): Boolean
external override fun eraseLegacyGroup(accountId: String): Boolean
external override fun eraseClosedGroup(accountId: String): Boolean
external override fun sizeCommunityInfo(): Long
external override fun sizeLegacyGroupInfo(): Long
external override fun sizeClosedGroup(): Long
external override fun size(): Long
external override fun all(): List<GroupInfo>
external override fun allCommunityInfo(): List<GroupInfo.CommunityGroupInfo>
external override fun allLegacyGroupInfo(): List<GroupInfo.LegacyGroupInfo>
external override fun allClosedGroupInfo(): List<GroupInfo.ClosedGroupInfo>
external override fun createGroup(): GroupInfo.ClosedGroupInfo
}
class GroupInfoConfig(pointer: Long): ConfigBase(pointer), Closeable {
companion object {
init {
System.loadLibrary("session_util")
}
interface ReadableGroupInfoConfig: ReadableConfig {
fun id(): AccountId
fun getDeleteAttachmentsBefore(): Long?
fun getDeleteBefore(): Long?
fun getExpiryTimer(): Long
fun getName(): String
fun getCreated(): Long?
fun getProfilePic(): UserPic
fun isDestroyed(): Boolean
fun getDescription(): String
fun storageNamespace(): Long
}
external fun newInstance(
pubKey: ByteArray?,
secretKey: ByteArray? = null,
initialDump: ByteArray = byteArrayOf()
): GroupInfoConfig
interface MutableGroupInfoConfig : ReadableGroupInfoConfig, MutableConfig {
fun setCreated(createdAt: Long)
fun setDeleteAttachmentsBefore(deleteBefore: Long)
fun setDeleteBefore(deleteBefore: Long)
fun setExpiryTimer(expireSeconds: Long)
fun setName(newName: String)
fun setDescription(newDescription: String)
fun setProfilePic(newProfilePic: UserPic)
fun destroyGroup()
}
class GroupInfoConfig private constructor(pointer: Long): ConfigBase(pointer), MutableGroupInfoConfig {
constructor(groupPubKey: ByteArray, groupAdminKey: ByteArray?, initialDump: ByteArray?)
: this(newInstance(groupPubKey, groupAdminKey, initialDump))
companion object {
private external fun newInstance(
pubKey: ByteArray,
secretKey: ByteArray?,
initialDump: ByteArray?
): Long
}
override fun namespace() = Namespace.CLOSED_GROUP_INFO()
external fun id(): AccountId
external fun destroyGroup()
external fun getCreated(): Long?
external fun getDeleteAttachmentsBefore(): Long?
external fun getDeleteBefore(): Long?
external fun getExpiryTimer(): Long
external fun getName(): String
external fun getProfilePic(): UserPic
external fun isDestroyed(): Boolean
external fun setCreated(createdAt: Long)
external fun setDeleteAttachmentsBefore(deleteBefore: Long)
external fun setDeleteBefore(deleteBefore: Long)
external fun setExpiryTimer(expireSeconds: Long)
external fun setName(newName: String)
external fun getDescription(): String
external fun setDescription(newDescription: String)
external fun setProfilePic(newProfilePic: UserPic)
external fun storageNamespace(): Long
override fun close() {
free()
}
external override fun id(): AccountId
external override fun destroyGroup()
external override fun getCreated(): Long?
external override fun getDeleteAttachmentsBefore(): Long?
external override fun getDeleteBefore(): Long?
external override fun getExpiryTimer(): Long
external override fun getName(): String
external override fun getProfilePic(): UserPic
external override fun isDestroyed(): Boolean
external override fun setCreated(createdAt: Long)
external override fun setDeleteAttachmentsBefore(deleteBefore: Long)
external override fun setDeleteBefore(deleteBefore: Long)
external override fun setExpiryTimer(expireSeconds: Long)
external override fun setName(newName: String)
external override fun getDescription(): String
external override fun setDescription(newDescription: String)
external override fun setProfilePic(newProfilePic: UserPic)
external override fun storageNamespace(): Long
}
class GroupMembersConfig(pointer: Long): ConfigBase(pointer), Closeable {
interface ReadableGroupMembersConfig: ReadableConfig {
fun all(): List<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 {
init {
System.loadLibrary("session_util")
}
external fun newInstance(
private external fun newInstance(
pubKey: ByteArray,
secretKey: ByteArray? = null,
initialDump: ByteArray = byteArrayOf()
): GroupMembersConfig
secretKey: ByteArray?,
initialDump: ByteArray?
): Long
}
constructor(groupPubKey: ByteArray, groupAdminKey: ByteArray?, initialDump: ByteArray?)
: this(newInstance(groupPubKey, groupAdminKey, initialDump))
override fun namespace() = Namespace.CLOSED_GROUP_MEMBERS()
external fun all(): Stack<GroupMember>
external fun erase(groupMember: GroupMember): Boolean
external fun erase(pubKeyHex: String): Boolean
external fun get(pubKeyHex: String): GroupMember?
external fun getOrConstruct(pubKeyHex: String): GroupMember
external fun set(groupMember: GroupMember)
override fun close() {
free()
}
external override fun all(): Stack<GroupMember>
external override fun erase(groupMember: GroupMember): Boolean
external override fun erase(pubKeyHex: String): Boolean
external override fun get(pubKeyHex: String): GroupMember?
external override fun getOrConstruct(pubKeyHex: String): GroupMember
external override fun set(groupMember: GroupMember)
}
sealed class ConfigSig(pointer: Long) : Config(pointer)
class GroupKeysConfig(pointer: Long): ConfigSig(pointer) {
interface ReadableGroupKeysConfig {
fun groupKeys(): Stack<ByteArray>
fun needsDump(): Boolean
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
}
interface MutableGroupKeysConfig : ReadableGroupKeysConfig {
fun makeSubAccount(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray
}
class GroupKeysConfig private constructor(pointer: Long): ConfigSig(pointer), MutableGroupKeysConfig {
companion object {
init {
System.loadLibrary("session_util")
}
external fun newInstance(
private external fun newInstance(
userSecretKey: ByteArray,
groupPublicKey: ByteArray,
groupSecretKey: ByteArray? = null,
initialDump: ByteArray = byteArrayOf(),
info: GroupInfoConfig,
members: GroupMembersConfig
): GroupKeysConfig
initialDump: ByteArray?,
infoPtr: Long,
members: Long
): Long
}
constructor(
userSecretKey: ByteArray,
groupPublicKey: ByteArray,
groupAdminKey: ByteArray?,
initialDump: ByteArray?,
info: GroupInfoConfig,
members: GroupMembersConfig
) : this(
newInstance(
userSecretKey,
groupPublicKey,
groupAdminKey,
initialDump,
info.pointer,
members.pointer
)
)
override fun namespace() = Namespace.ENCRYPTION_KEYS()
external fun groupKeys(): Stack<ByteArray>
external fun needsDump(): Boolean
external fun dump(): ByteArray
external override fun groupKeys(): Stack<ByteArray>
external override fun needsDump(): Boolean
external override fun dump(): ByteArray
external fun loadKey(message: ByteArray,
hash: String,
timestampMs: Long,
info: GroupInfoConfig,
members: GroupMembersConfig): Boolean
external fun needsRekey(): Boolean
external fun pendingKey(): ByteArray?
external fun supplementFor(userSessionId: String): ByteArray
external fun pendingConfig(): ByteArray?
external fun currentHashes(): List<String>
external fun rekey(info: GroupInfoConfig, members: GroupMembersConfig): ByteArray
override fun close() {
free()
}
infoPtr: Long,
membersPtr: Long): Boolean
external override fun needsRekey(): Boolean
external override fun pendingKey(): ByteArray?
external override fun supplementFor(userSessionId: String): ByteArray
external override fun pendingConfig(): ByteArray?
external override fun currentHashes(): List<String>
external fun rekey(infoPtr: Long, membersPtr: Long): ByteArray
external fun encrypt(plaintext: ByteArray): ByteArray
external fun decrypt(ciphertext: ByteArray): Pair<ByteArray, AccountId>?
external override fun encrypt(plaintext: ByteArray): ByteArray
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 fun getSubAccountToken(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray
external override fun makeSubAccount(sessionId: AccountId, canWrite: Boolean, canDelete: Boolean): ByteArray
external override fun getSubAccountToken(sessionId: AccountId, canWrite: Boolean, canDelete: Boolean): ByteArray
external fun subAccountSign(message: ByteArray, signingValue: ByteArray): SwarmAuth
external override fun subAccountSign(message: ByteArray, signingValue: ByteArray): SwarmAuth
external fun currentGeneration(): Int
external override fun currentGeneration(): Int
data class SwarmAuth(
val subAccount: String,
val subAccountSig: String,
val signature: String
)
}
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 getMessageReceiveJob(messageReceiveJobID: String): Job?
fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): Job?
fun getConfigSyncJob(destination: Destination): Job?
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
fun isJobCanceled(job: Job): Boolean
fun cancelPendingMessageSendJobs(threadID: Long)
@ -269,7 +268,6 @@ interface StorageProtocol {
)
// Shared configs
fun notifyConfigUpdates(forConfigObject: Config, messageTimestamp: Long)
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
fun isCheckingCommunityRequests(): Boolean

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.flow.firstOrNull
import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.GroupKeysConfig
import network.loki.messenger.libsession_util.ReadableGroupKeysConfig
import network.loki.messenger.libsession_util.util.GroupMember
import network.loki.messenger.libsession_util.util.Sodium
import org.session.libsession.messaging.messages.Destination
@ -17,8 +17,8 @@ import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.snode.OwnedSwarmAuth
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.withGroupConfigsOrNull
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
import org.session.libsignal.utilities.AccountId
@ -61,12 +61,8 @@ class RemoveGroupMemberHandler(
}
private suspend fun processPendingMemberRemoval() {
val userGroups = checkNotNull(configFactory.userGroups) {
"User groups config is null"
}
// Run the removal process for each group in parallel
val removalTasks = userGroups.allClosedGroupInfo()
val removalTasks = configFactory.withUserConfigs { it.userGroups.allClosedGroupInfo() }
.asSequence()
.filter { it.hasAdminKey() }
.associate { group ->
@ -89,7 +85,7 @@ class RemoveGroupMemberHandler(
}
}
private fun processPendingRemovalsForGroup(
private suspend fun processPendingRemovalsForGroup(
groupAccountId: AccountId,
groupName: String,
adminKey: ByteArray
@ -100,11 +96,11 @@ class RemoveGroupMemberHandler(
ed25519PrivateKey = adminKey
)
configFactory.withGroupConfigsOrNull(groupAccountId) withConfig@ { info, members, keys ->
val pendingRemovals = members.all().filter { it.removed }
val batchCalls = configFactory.withGroupConfigs(groupAccountId) { configs ->
val pendingRemovals = configs.groupMembers.all().filter { it.removed }
if (pendingRemovals.isEmpty()) {
// Skip if there are no pending removals
return@withConfig
return@withGroupConfigs emptyList()
}
Log.d(TAG, "Processing ${pendingRemovals.size} pending removals for group $groupName")
@ -113,28 +109,28 @@ class RemoveGroupMemberHandler(
// 1. Revoke the member's sub key (by adding the key to a "revoked list" under the hood)
// 2. Send a message to a special namespace to inform the removed members they have been removed
// 3. Conditionally, delete removed-members' messages from the group's message store, if that option is selected by the actioning admin
val seqCalls = ArrayList<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.
seqCalls += checkNotNull(
calls += checkNotNull(
SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest(
groupAdminAuth = swarmAuth,
subAccountTokens = pendingRemovals.map {
keys.getSubAccountToken(AccountId(it.sessionId))
configs.groupKeys.getSubAccountToken(AccountId(it.sessionId))
}
)
) { "Fail to create a revoke request" }
// Call No 2. Send a message to the removed members
seqCalls += SnodeAPI.buildAuthenticatedStoreBatchInfo(
calls += SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = Namespace.REVOKED_GROUP_MESSAGES(),
message = buildGroupKickMessage(groupAccountId.hexString, pendingRemovals, keys, adminKey),
message = buildGroupKickMessage(groupAccountId.hexString, pendingRemovals, configs.groupKeys, adminKey),
auth = swarmAuth,
)
// Call No 3. Conditionally remove the message from the group's message store
if (pendingRemovals.any { it.shouldRemoveMessages }) {
seqCalls += SnodeAPI.buildAuthenticatedStoreBatchInfo(
calls += SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = Namespace.CLOSED_GROUP_MESSAGES(),
message = buildDeleteGroupMemberContentMessage(
groupAccountId = groupAccountId.hexString,
@ -147,9 +143,17 @@ class RemoveGroupMemberHandler(
)
}
// Make the call:
SnodeAPI.getSingleTargetSnode(groupAccountId.hexString)
calls
}
if (batchCalls.isEmpty()) {
return
}
val node = SnodeAPI.getSingleTargetSnode(groupAccountId.hexString).await()
SnodeAPI.getBatchResponse(node, groupAccountId.hexString, batchCalls, true)
//TODO: Handle message removal
}
private fun buildDeleteGroupMemberContentMessage(
@ -178,7 +182,7 @@ class RemoveGroupMemberHandler(
private fun buildGroupKickMessage(
groupAccountId: String,
pendingRemovals: List<GroupMember>,
keys: GroupKeysConfig,
keys: ReadableGroupKeysConfig,
adminKey: ByteArray
) = SnodeMessage(
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 openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher()
private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val configDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob()
private val queue = Channel<Job>(UNLIMITED)
@ -117,20 +116,14 @@ class JobQueue : JobDelegate {
val txQueue = Channel<Job>(capacity = UNLIMITED)
val mediaQueue = 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 txJob = processWithDispatcher(txQueue, txDispatcher, "tx")
val mediaJob = processWithDispatcher(mediaQueue, rxMediaDispatcher, "media")
val openGroupJob = processWithOpenGroupDispatcher(openGroupQueue, openGroupDispatcher, "openGroup")
val configJob = processWithDispatcher(configQueue, configDispatcher, "configDispatcher")
while (isActive) {
when (val job = queue.receive()) {
is InviteContactsJob,
is ConfigurationSyncJob -> {
configQueue.send(job)
}
is NotifyPNServerJob,
is AttachmentUploadJob,
is GroupLeavingJob,
@ -167,7 +160,6 @@ class JobQueue : JobDelegate {
txJob.cancel()
mediaJob.cancel()
openGroupJob.cancel()
configJob.cancel()
}
}
@ -239,8 +231,6 @@ class JobQueue : JobDelegate {
BackgroundGroupAddJob.KEY,
OpenGroupDeleteJob.KEY,
RetrieveProfileAvatarJob.KEY,
ConfigurationSyncJob.KEY,
InviteContactsJob.KEY,
GroupLeavingJob.KEY,
LibSessionGroupLeavingJob.KEY
)

View File

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

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

View File

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

View File

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

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

View File

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

View File

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