mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-28 20:45:17 +00:00
Config revamp WIP
This commit is contained in:
parent
7eb615f8dc
commit
771d63e902
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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 ->
|
||||
|
@ -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) }
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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}%');
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
39
libsession-util/src/main/cpp/config_common.cpp
Normal file
39
libsession-util/src/main/cpp/config_common.cpp
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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()!!
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
//}
|
Loading…
Reference in New Issue
Block a user