Config revamp WIP

This commit is contained in:
SessionHero01
2024-09-26 15:43:45 +10:00
parent 7eb615f8dc
commit 771d63e902
58 changed files with 1831 additions and 2532 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,40 +1,57 @@
package org.thoughtcrime.securesms.dependencies
import android.content.Context
import android.os.Trace
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import network.loki.messenger.libsession_util.Config
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.GroupInfoConfig
import network.loki.messenger.libsession_util.GroupKeysConfig
import network.loki.messenger.libsession_util.GroupMembersConfig
import network.loki.messenger.libsession_util.MutableContacts
import network.loki.messenger.libsession_util.MutableConversationVolatileConfig
import network.loki.messenger.libsession_util.MutableUserGroupsConfig
import network.loki.messenger.libsession_util.MutableUserProfile
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupInfo
import network.loki.messenger.libsession_util.util.Sodium
import org.session.libsession.messaging.messages.Destination
import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.snode.OwnedSwarmAuth
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SwarmAuth
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.ConfigFactoryUpdateListener
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsession.utilities.GroupConfigs
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MutableGroupConfigs
import org.session.libsession.utilities.MutableUserConfigs
import org.session.libsession.utilities.UserConfigs
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.ConfigDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import java.util.concurrent.ConcurrentHashMap
class ConfigFactory(
private val context: Context,
private val configDatabase: ConfigDatabase,
/** <ed25519 secret key,33 byte prefixed public key (hex encoded)> */
private val maybeGetUserInfo: () -> Pair<ByteArray, String>?
) :
ConfigFactoryProtocol {
private val threadDb: ThreadDatabase,
private val storage: StorageProtocol,
) : ConfigFactoryProtocol {
companion object {
// This is a buffer period within which we will process messages which would result in a
// config change, any message which would normally result in a config change which was sent
@@ -43,351 +60,300 @@ class ConfigFactory(
const val configChangeBufferPeriod: Long = (2 * 60 * 1000)
}
fun keyPairChanged() { // this should only happen restoring or clearing datac
_userConfig?.free()
_contacts?.free()
_convoVolatileConfig?.free()
_userConfig = null
_contacts = null
_convoVolatileConfig = null
init {
System.loadLibrary("session_util")
}
private val userLock = Object()
private var _userConfig: UserProfile? = null
private val contactsLock = Object()
private var _contacts: Contacts? = null
private val convoVolatileLock = Object()
private var _convoVolatileConfig: ConversationVolatileConfig? = null
private val userGroupsLock = Object()
private var _userGroups: UserGroupsConfig? = null
private class UserConfigsImpl(
userEd25519SecKey: ByteArray,
private val userAccountId: AccountId,
private val configDatabase: ConfigDatabase,
storage: StorageProtocol,
threadDb: ThreadDatabase,
contactsDump: ByteArray? = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.CONTACTS_VARIANT,
userAccountId.hexString
),
userGroupsDump: ByteArray? = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.USER_GROUPS_VARIANT,
userAccountId.hexString
),
userProfileDump: ByteArray? = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.USER_PROFILE_VARIANT,
userAccountId.hexString
),
convoInfoDump: ByteArray? = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.CONVO_INFO_VARIANT,
userAccountId.hexString
)
) : MutableUserConfigs {
override val contacts = Contacts(
ed25519SecretKey = userEd25519SecKey,
initialDump = contactsDump,
)
private val isConfigForcedOn by lazy { TextSecurePreferences.hasForcedNewConfig(context) }
override val userGroups = UserGroupsConfig(
ed25519SecretKey = userEd25519SecKey,
initialDump = userGroupsDump
)
override val userProfile = UserProfile(
ed25519SecretKey = userEd25519SecKey,
initialDump = userProfileDump
)
override val convoInfoVolatile = ConversationVolatileConfig(
ed25519SecretKey = userEd25519SecKey,
initialDump = convoInfoDump,
)
private val listeners: MutableList<ConfigFactoryUpdateListener> = mutableListOf()
init {
if (contactsDump == null) {
contacts.initFrom(storage)
}
private val _configUpdateNotifications = MutableSharedFlow<Unit>(
if (userGroupsDump == null) {
userGroups.initFrom(storage)
}
if (userProfileDump == null) {
userProfile.initFrom(storage)
}
if (convoInfoDump == null) {
convoInfoVolatile.initFrom(storage, threadDb)
}
}
/**
* Persists the config if it is dirty and returns the list of classes that were persisted
*/
fun persistIfDirty(): Boolean {
return sequenceOf(
contacts to ConfigDatabase.CONTACTS_VARIANT,
userGroups to ConfigDatabase.USER_GROUPS_VARIANT,
userProfile to ConfigDatabase.USER_PROFILE_VARIANT,
convoInfoVolatile to ConfigDatabase.CONVO_INFO_VARIANT
).fold(false) { acc, (config, variant) ->
if (config.needsDump()) {
configDatabase.storeConfig(
variant = variant,
publicKey = userAccountId.hexString,
data = config.dump(),
timestamp = SnodeAPI.nowWithOffset
)
true
} else {
acc
}
}
}
}
private class GroupConfigsImpl(
userEd25519SecKey: ByteArray,
private val groupAccountId: AccountId,
groupAdminKey: ByteArray?,
private val configDatabase: ConfigDatabase
) : MutableGroupConfigs {
override val groupInfo = GroupInfoConfig(
groupPubKey = groupAccountId.pubKeyBytes,
groupAdminKey = groupAdminKey,
initialDump = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.INFO_VARIANT,
groupAccountId.hexString
)
)
override val groupMembers = GroupMembersConfig(
groupPubKey = groupAccountId.pubKeyBytes,
groupAdminKey = groupAdminKey,
initialDump = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.MEMBER_VARIANT,
groupAccountId.hexString
)
)
override val groupKeys = GroupKeysConfig(
userSecretKey = userEd25519SecKey,
groupPublicKey = groupAccountId.pubKeyBytes,
groupAdminKey = groupAdminKey,
initialDump = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.KEYS_VARIANT,
groupAccountId.hexString
),
info = groupInfo,
members = groupMembers
)
fun persistIfDirty(): Boolean {
if (groupInfo.needsDump() || groupMembers.needsDump() || groupKeys.needsDump()) {
configDatabase.storeGroupConfigs(
publicKey = groupAccountId.hexString,
keysConfig = groupKeys.dump(),
infoConfig = groupInfo.dump(),
memberConfig = groupMembers.dump(),
timestamp = SnodeAPI.nowWithOffset
)
return true
}
return false
}
override fun loadKeys(message: ByteArray, hash: String, timestamp: Long): Boolean {
return groupKeys.loadKey(message, hash, timestamp, groupInfo.pointer, groupMembers.pointer)
}
override fun rekeys() {
groupKeys.rekey(groupInfo.pointer, groupMembers.pointer)
}
}
private val userConfigs = ConcurrentHashMap<AccountId, UserConfigsImpl>()
private val groupConfigs = ConcurrentHashMap<AccountId, GroupConfigsImpl>()
private val _configUpdateNotifications = MutableSharedFlow<ConfigUpdateNotification>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
override val configUpdateNotifications get() = _configUpdateNotifications
fun registerListener(listener: ConfigFactoryUpdateListener) {
listeners += listener
}
private fun requiresCurrentUserAccountId(): AccountId =
AccountId(requireNotNull(storage.getUserPublicKey()) {
"No logged in user"
})
fun unregisterListener(listener: ConfigFactoryUpdateListener) {
listeners -= listener
}
private inline fun <T> synchronizedWithLog(lock: Any, body: () -> T): T {
Trace.beginSection("synchronizedWithLog")
val result = synchronized(lock) {
body()
private fun requiresCurrentUserED25519SecKey(): ByteArray =
requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) {
"No logged in user"
}
override fun <T> withUserConfigs(cb: (UserConfigs) -> T): T {
val userAccountId = requiresCurrentUserAccountId()
val configs = userConfigs.getOrPut(userAccountId) {
UserConfigsImpl(
requiresCurrentUserED25519SecKey(),
userAccountId,
threadDb = threadDb,
configDatabase = configDatabase,
storage = storage
)
}
return synchronized(configs) {
cb(configs)
}
Trace.endSection()
return result
}
override val user: UserProfile?
get() = synchronizedWithLog(userLock) {
if (_userConfig == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
val userDump = configDatabase.retrieveConfigAndHashes(
SharedConfigMessage.Kind.USER_PROFILE.name,
publicKey
)
_userConfig = if (userDump != null) {
UserProfile.newInstance(secretKey, userDump)
} else {
ConfigurationMessageUtilities.generateUserProfileConfigDump()?.let { dump ->
UserProfile.newInstance(secretKey, dump)
} ?: UserProfile.newInstance(secretKey)
}
override fun <T> withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T {
return withUserConfigs { configs ->
val result = cb(configs as UserConfigsImpl)
if (configs.persistIfDirty()) {
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.UserConfigs)
}
_userConfig
result
}
}
override fun <T> withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T {
val configs = groupConfigs.getOrPut(groupId) {
val groupAdminKey = requireNotNull(withUserConfigs {
it.userGroups.getClosedGroup(groupId.hexString)
}) {
"Group not found"
}.adminKey
GroupConfigsImpl(
requiresCurrentUserED25519SecKey(),
groupId,
groupAdminKey,
configDatabase
)
}
override val contacts: Contacts?
get() = synchronizedWithLog(contactsLock) {
if (_contacts == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
val contactsDump = configDatabase.retrieveConfigAndHashes(
SharedConfigMessage.Kind.CONTACTS.name,
publicKey
)
_contacts = if (contactsDump != null) {
Contacts.newInstance(secretKey, contactsDump)
} else {
ConfigurationMessageUtilities.generateContactConfigDump()?.let { dump ->
Contacts.newInstance(secretKey, dump)
} ?: Contacts.newInstance(secretKey)
}
return synchronized(configs) {
cb(configs)
}
}
override fun <T> withMutableGroupConfigs(
groupId: AccountId,
cb: (MutableGroupConfigs) -> T
): T {
return withGroupConfigs(groupId) { configs ->
val result = cb(configs as GroupConfigsImpl)
if (configs.persistIfDirty()) {
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsUpdated(groupId))
}
_contacts
result
}
override val convoVolatile: ConversationVolatileConfig?
get() = synchronizedWithLog(convoVolatileLock) {
if (_convoVolatileConfig == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
val convoDump = configDatabase.retrieveConfigAndHashes(
SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name,
publicKey
)
_convoVolatileConfig = if (convoDump != null) {
ConversationVolatileConfig.newInstance(secretKey, convoDump)
} else {
ConfigurationMessageUtilities.generateConversationVolatileDump(context)
?.let { dump ->
ConversationVolatileConfig.newInstance(secretKey, dump)
} ?: ConversationVolatileConfig.newInstance(secretKey)
}
}
_convoVolatileConfig
}
override val userGroups: UserGroupsConfig?
get() = synchronizedWithLog(userGroupsLock) {
if (_userGroups == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
val userGroupsDump = configDatabase.retrieveConfigAndHashes(
SharedConfigMessage.Kind.GROUPS.name,
publicKey
)
_userGroups = if (userGroupsDump != null) {
UserGroupsConfig.Companion.newInstance(secretKey, userGroupsDump)
} else {
ConfigurationMessageUtilities.generateUserGroupDump(context)?.let { dump ->
UserGroupsConfig.Companion.newInstance(secretKey, dump)
} ?: UserGroupsConfig.newInstance(secretKey)
}
}
_userGroups
}
private fun getGroupInfo(groupSessionId: AccountId) = userGroups?.getClosedGroup(groupSessionId.hexString)
override fun getGroupInfoConfig(groupSessionId: AccountId): GroupInfoConfig? = getGroupInfo(groupSessionId)?.let { groupInfo ->
// get any potential initial dumps
val dump = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.INFO_VARIANT,
groupSessionId.hexString
) ?: byteArrayOf()
GroupInfoConfig.newInstance(groupSessionId.pubKeyBytes, groupInfo.adminKey, dump)
}
override fun getGroupKeysConfig(groupSessionId: AccountId,
info: GroupInfoConfig?,
members: GroupMembersConfig?,
free: Boolean): GroupKeysConfig? = getGroupInfo(groupSessionId)?.let { groupInfo ->
// Get the user info or return early
val (userSk, _) = maybeGetUserInfo() ?: return@let null
// Get the group info or return early
val usedInfo = info ?: getGroupInfoConfig(groupSessionId) ?: return@let null
// Get the group members or return early
val usedMembers = members ?: getGroupMemberConfig(groupSessionId) ?: return@let null
// Get the dump or empty
val dump = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.KEYS_VARIANT,
groupSessionId.hexString
) ?: byteArrayOf()
// Put it all together
val keys = GroupKeysConfig.newInstance(
userSk,
groupSessionId.pubKeyBytes,
groupInfo.adminKey,
dump,
usedInfo,
usedMembers
)
if (free) {
info?.free()
members?.free()
override fun removeGroup(groupId: AccountId) {
withMutableUserConfigs {
it.userGroups.eraseClosedGroup(groupId.hexString)
}
if (usedInfo !== info) usedInfo.free()
if (usedMembers !== members) usedMembers.free()
keys
}
override fun getGroupMemberConfig(groupSessionId: AccountId): GroupMembersConfig? = getGroupInfo(groupSessionId)?.let { groupInfo ->
// Get initial dump if we have one
val dump = configDatabase.retrieveConfigAndHashes(
ConfigDatabase.MEMBER_VARIANT,
groupSessionId.hexString
) ?: byteArrayOf()
GroupMembersConfig.newInstance(
groupSessionId.pubKeyBytes,
groupInfo.adminKey,
dump
)
}
override fun constructGroupKeysConfig(
groupSessionId: AccountId,
info: GroupInfoConfig,
members: GroupMembersConfig
): GroupKeysConfig? = getGroupInfo(groupSessionId)?.let { groupInfo ->
val (userSk, _) = maybeGetUserInfo() ?: return null
GroupKeysConfig.newInstance(
userSk,
groupSessionId.pubKeyBytes,
groupInfo.adminKey,
info = info,
members = members
)
}
override fun userSessionId(): AccountId? {
return maybeGetUserInfo()?.second?.let(::AccountId)
}
override fun maybeDecryptForUser(encoded: ByteArray, domain: String, closedGroupSessionId: AccountId): ByteArray? {
val secret = maybeGetUserInfo()?.first ?: run {
Log.e("ConfigFactory", "No user ed25519 secret key decrypting a message for us")
return null
if (groupConfigs.remove(groupId) != null) {
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsDeleted(groupId))
}
configDatabase.deleteGroupConfigs(groupId)
}
override fun maybeDecryptForUser(
encoded: ByteArray,
domain: String,
closedGroupSessionId: AccountId
): ByteArray? {
return Sodium.decryptForMultipleSimple(
encoded = encoded,
ed25519SecretKey = secret,
ed25519SecretKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) {
"No logged in user"
},
domain = domain,
senderPubKey = Sodium.ed25519PkToCurve25519(closedGroupSessionId.pubKeyBytes)
)
}
override fun getUserConfigs(): List<ConfigBase> =
listOfNotNull(user, contacts, convoVolatile, userGroups)
private fun persistUserConfigDump(timestamp: Long) = synchronized(userLock) {
val dumped = user?.dump() ?: return
val (_, publicKey) = maybeGetUserInfo() ?: return
configDatabase.storeConfig(
SharedConfigMessage.Kind.USER_PROFILE.name,
publicKey,
dumped,
timestamp
)
}
private fun persistContactsConfigDump(timestamp: Long) = synchronized(contactsLock) {
val dumped = contacts?.dump() ?: return
val (_, publicKey) = maybeGetUserInfo() ?: return
configDatabase.storeConfig(
SharedConfigMessage.Kind.CONTACTS.name,
publicKey,
dumped,
timestamp
)
}
private fun persistConvoVolatileConfigDump(timestamp: Long) = synchronized(convoVolatileLock) {
val dumped = convoVolatile?.dump() ?: return
val (_, publicKey) = maybeGetUserInfo() ?: return
configDatabase.storeConfig(
SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name,
publicKey,
dumped,
timestamp
)
}
private fun persistUserGroupsConfigDump(timestamp: Long) = synchronized(userGroupsLock) {
val dumped = userGroups?.dump() ?: return
val (_, publicKey) = maybeGetUserInfo() ?: return
configDatabase.storeConfig(
SharedConfigMessage.Kind.GROUPS.name,
publicKey,
dumped,
timestamp
)
}
fun persistGroupConfigDump(forConfigObject: ConfigBase, groupSessionId: AccountId, timestamp: Long) = synchronized(userGroupsLock) {
val dumped = forConfigObject.dump()
val variant = when (forConfigObject) {
is GroupMembersConfig -> ConfigDatabase.MEMBER_VARIANT
is GroupInfoConfig -> ConfigDatabase.INFO_VARIANT
else -> throw Exception("Shouldn't be called")
}
configDatabase.storeConfig(
variant,
groupSessionId.hexString,
dumped,
timestamp
)
_configUpdateNotifications.tryEmit(Unit)
}
override fun persist(forConfigObject: Config, timestamp: Long, forPublicKey: String?) {
try {
if (forConfigObject is ConfigBase && !forConfigObject.needsDump() || forConfigObject is GroupKeysConfig && !forConfigObject.needsDump()) {
Log.d("ConfigFactory", "Don't need to persist ${forConfigObject.javaClass} for $forPublicKey pubkey")
return
}
listeners.forEach { listener ->
listener.notifyUpdates(forConfigObject, timestamp)
}
when (forConfigObject) {
is UserProfile -> persistUserConfigDump(timestamp)
is Contacts -> persistContactsConfigDump(timestamp)
is ConversationVolatileConfig -> persistConvoVolatileConfigDump(timestamp)
is UserGroupsConfig -> persistUserGroupsConfigDump(timestamp)
is GroupMembersConfig -> persistGroupConfigDump(forConfigObject, AccountId(forPublicKey!!), timestamp)
is GroupInfoConfig -> persistGroupConfigDump(forConfigObject, AccountId(forPublicKey!!), timestamp)
else -> throw UnsupportedOperationException("Can't support type of ${forConfigObject::class.simpleName} yet")
}
_configUpdateNotifications.tryEmit(Unit)
} catch (e: Exception) {
Log.e("Loki", "failed to persist ${forConfigObject.javaClass.simpleName}", e)
}
}
override fun conversationInConfig(
publicKey: String?,
groupPublicKey: String?,
openGroupId: String?,
visibleOnly: Boolean
): Boolean {
val (_, userPublicKey) = maybeGetUserInfo() ?: return true
val userPublicKey = storage.getUserPublicKey() ?: return false
if (openGroupId != null) {
val userGroups = userGroups ?: return false
val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context)
val openGroup =
get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false
// Not handling the `hidden` behaviour for communities so just indicate the existence
return (userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null)
return withUserConfigs {
it.userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null
}
} else if (groupPublicKey != null) {
val userGroups = userGroups ?: return false
// Not handling the `hidden` behaviour for legacy groups so just indicate the existence
return if (groupPublicKey.startsWith(IdPrefix.GROUP.value)) {
userGroups.getClosedGroup(groupPublicKey) != null
} else {
userGroups.getLegacyGroupInfo(groupPublicKey) != null
return withUserConfigs {
if (groupPublicKey.startsWith(IdPrefix.GROUP.value)) {
it.userGroups.getClosedGroup(groupPublicKey) != null
} else {
it.userGroups.getLegacyGroupInfo(groupPublicKey) != null
}
}
} else if (publicKey == userPublicKey) {
val user = user ?: return false
return (!visibleOnly || user.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN)
return withUserConfigs {
!visibleOnly || it.userProfile.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN
}
} else if (publicKey != null) {
val contacts = contacts ?: return false
val targetContact = contacts.get(publicKey) ?: return false
return (!visibleOnly || targetContact.priority != ConfigBase.PRIORITY_HIDDEN)
return withUserConfigs {
(!visibleOnly || it.contacts.get(publicKey)?.priority != ConfigBase.PRIORITY_HIDDEN)
}
} else {
return false
}
return false
}
override fun canPerformChange(
@@ -402,32 +368,192 @@ class ConfigFactory(
return (changeTimestampMs >= (lastUpdateTimestampMs - configChangeBufferPeriod))
}
override fun saveGroupConfigs(
groupKeys: GroupKeysConfig,
groupInfo: GroupInfoConfig,
groupMembers: GroupMembersConfig
) {
val pubKey = groupInfo.id().hexString
val timestamp = SnodeAPI.nowWithOffset
override fun getGroupAuth(groupId: AccountId): SwarmAuth? {
val (adminKey, authData) = withUserConfigs {
val group = it.userGroups.getClosedGroup(groupId.hexString)
group?.adminKey to group?.authData
}
// this would be nicer with a .any iteration or something but the base types don't line up
val anyNeedDump = groupKeys.needsDump() || groupInfo.needsDump() || groupMembers.needsDump()
if (!anyNeedDump) return Log.d("ConfigFactory", "Group config doesn't need dump, skipping")
else Log.d("ConfigFactory", "Group config needs dump, storing and notifying")
configDatabase.storeGroupConfigs(pubKey, groupKeys.dump(), groupInfo.dump(), groupMembers.dump(), timestamp)
_configUpdateNotifications.tryEmit(Unit)
return if (adminKey != null) {
OwnedSwarmAuth.ofClosedGroup(groupId, adminKey)
} else if (authData != null) {
GroupSubAccountSwarmAuth(groupId, this, authData)
} else {
null
}
}
override fun removeGroup(closedGroupId: AccountId) {
val groups = userGroups ?: return
groups.eraseClosedGroup(closedGroupId.hexString)
persist(groups, SnodeAPI.nowWithOffset)
configDatabase.deleteGroupConfigs(closedGroupId)
fun clearAll() {
//TODO: clear all configsr
}
override fun scheduleUpdate(destination: Destination) {
// there's probably a better way to do this
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(destination)
private class GroupSubAccountSwarmAuth(
override val accountId: AccountId,
val factory: ConfigFactory,
val authData: ByteArray,
) : SwarmAuth {
override val ed25519PublicKeyHex: String?
get() = null
override fun sign(data: ByteArray): Map<String, String> {
return factory.withGroupConfigs(accountId) {
val auth = it.groupKeys.subAccountSign(data, authData)
buildMap {
put("subaccount", auth.subAccount)
put("subaccount_sig", auth.subAccountSig)
put("signature", auth.signature)
}
}
}
override fun signForPushRegistry(data: ByteArray): Map<String, String> {
return factory.withGroupConfigs(accountId) {
val auth = it.groupKeys.subAccountSign(data, authData)
buildMap {
put("subkey_tag", auth.subAccount)
put("signature", auth.signature)
}
}
}
}
}
/**
* Sync group data from our local database
*/
private fun MutableUserGroupsConfig.initFrom(storage: StorageProtocol) {
storage
.getAllOpenGroups()
.values
.asSequence()
.mapNotNull { openGroup ->
val (baseUrl, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@mapNotNull null
val pubKeyHex = Hex.toStringCondensed(pubKey)
val baseInfo = BaseCommunityInfo(baseUrl, room, pubKeyHex)
val threadId = storage.getThreadId(openGroup) ?: return@mapNotNull null
val isPinned = storage.isPinned(threadId)
GroupInfo.CommunityGroupInfo(baseInfo, if (isPinned) 1 else 0)
}
.forEach(this::set)
storage
.getAllGroups(includeInactive = false)
.asSequence().filter { it.isLegacyClosedGroup && it.isActive && it.members.size > 1 }
.mapNotNull { group ->
val groupAddress = Address.fromSerialized(group.encodedId)
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupAddress.serialize()).toHexString()
val recipient = storage.getRecipientSettings(groupAddress) ?: return@mapNotNull null
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return@mapNotNull null
val threadId = storage.getThreadId(group.encodedId)
val isPinned = threadId?.let { storage.isPinned(threadId) } ?: false
val admins = group.admins.associate { it.serialize() to true }
val members = group.members.filterNot { it.serialize() !in admins.keys }.associate { it.serialize() to false }
GroupInfo.LegacyGroupInfo(
accountId = groupPublicKey,
name = group.title,
members = admins + members,
priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = encryptionKeyPair.privateKey.serialize(),
disappearingTimer = recipient.expireMessages.toLong(),
joinedAt = (group.formationTimestamp / 1000L)
)
}
.forEach(this::set)
}
private fun MutableConversationVolatileConfig.initFrom(storage: StorageProtocol, threadDb: ThreadDatabase) {
threadDb.approvedConversationList.use { cursor ->
val reader = threadDb.readerFor(cursor)
var current = reader.next
while (current != null) {
val recipient = current.recipient
val contact = when {
recipient.isCommunityRecipient -> {
val openGroup = storage.getOpenGroup(current.threadId) ?: continue
val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue
getOrConstructCommunity(base, room, pubKey)
}
recipient.isClosedGroupV2Recipient -> {
// It's probably safe to assume there will never be a case where new closed groups will ever be there before a dump is created...
// but just in case...
getOrConstructClosedGroup(recipient.address.serialize())
}
recipient.isLegacyClosedGroupRecipient -> {
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
getOrConstructLegacyGroup(groupPublicKey)
}
recipient.isContactRecipient -> {
if (recipient.isLocalNumber) null // this is handled by the user profile NTS data
else if (recipient.isOpenGroupInboxRecipient) null // specifically exclude
else if (!recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) null
else getOrConstructOneToOne(recipient.address.serialize())
}
else -> null
}
if (contact == null) {
current = reader.next
continue
}
contact.lastRead = current.lastSeen
contact.unread = false
set(contact)
current = reader.next
}
}
}
private fun MutableUserProfile.initFrom(storage: StorageProtocol) {
val ownPublicKey = storage.getUserPublicKey() ?: return
val config = ConfigurationMessage.getCurrent(listOf()) ?: return
setName(config.displayName)
val picUrl = config.profilePicture
val picKey = config.profileKey
if (!picUrl.isNullOrEmpty() && picKey.isNotEmpty()) {
setPic(UserPic(picUrl, picKey))
}
val ownThreadId = storage.getThreadId(Address.fromSerialized(ownPublicKey))
setNtsPriority(
if (ownThreadId != null)
if (storage.isPinned(ownThreadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
else ConfigBase.PRIORITY_HIDDEN
)
}
private fun MutableContacts.initFrom(storage: StorageProtocol) {
val localUserKey = storage.getUserPublicKey() ?: return
val contactsWithSettings = storage.getAllContacts().filter { recipient ->
recipient.accountID != localUserKey && recipient.accountID.startsWith(IdPrefix.STANDARD.value)
&& storage.getThreadId(recipient.accountID) != null
}.map { contact ->
val address = Address.fromSerialized(contact.accountID)
val thread = storage.getThreadId(address)
val isPinned = if (thread != null) {
storage.isPinned(thread)
} else false
Triple(contact, storage.getRecipientSettings(address)!!, isPinned)
}
for ((contact, settings, isPinned) in contactsWithSettings) {
val url = contact.profilePictureURL
val key = contact.profilePictureEncryptionKey
val userPic = if (url.isNullOrEmpty() || key?.isNotEmpty() != true) {
null
} else {
UserPic(url, key)
}
val contactInfo = Contact(
id = contact.accountID,
name = contact.name.orEmpty(),
nickname = contact.nickname.orEmpty(),
blocked = settings.isBlocked,
approved = settings.isApproved,
approvedMe = settings.hasApprovedMe(),
profilePicture = userPic ?: UserPic.DEFAULT,
priority = if (isPinned) 1 else 0,
expiryMode = if (settings.expireMessages == 0) ExpiryMode.NONE else ExpiryMode.AfterRead(settings.expireMessages.toLong())
)
set(contactInfo)
}
}

View File

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

View File

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

View File

@@ -34,22 +34,25 @@ object ClosedGroupManager {
}
fun ConfigFactory.updateLegacyGroup(group: GroupRecord) {
val groups = userGroups ?: return
if (!group.isLegacyClosedGroup) return
val storage = MessagingModuleConfiguration.shared.storage
val threadId = storage.getThreadId(group.encodedId) ?: return
val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId())
val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey)
val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize))
val toSet = legacyInfo.copy(
members = latestMemberMap,
name = group.title,
priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = latestKeyPair.privateKey.serialize()
)
groups.set(toSet)
}
withMutableUserConfigs {
val groups = it.userGroups
val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey)
val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize))
val toSet = legacyInfo.copy(
members = latestMemberMap,
name = group.title,
priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = latestKeyPair.privateKey.serialize()
)
groups.set(toSet)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,254 +1,10 @@
package org.thoughtcrime.securesms.util
import android.content.Context
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupInfo
import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.ConfigurationSyncJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.WindowDebouncer
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import java.util.Timer
import java.util.concurrent.ConcurrentLinkedDeque
object ConfigurationMessageUtilities {
private const val TAG = "ConfigMessageUtils"
private val debouncer = WindowDebouncer(3000, Timer())
private val destinationUpdater = Any()
private val pendingDestinations = ConcurrentLinkedDeque<Destination>()
private fun scheduleConfigSync(destination: Destination) {
synchronized(destinationUpdater) {
pendingDestinations.add(destination)
}
debouncer.publish {
// don't schedule job if we already have one
val storage = MessagingModuleConfiguration.shared.storage
val configFactory = MessagingModuleConfiguration.shared.configFactory
val destinations = synchronized(destinationUpdater) {
val objects = pendingDestinations.toList()
pendingDestinations.clear()
objects
}
destinations.forEach { destination ->
if (destination is Destination.ClosedGroup) {
// ensure we have the appropriate admin keys, skip this destination otherwise
val group = configFactory.userGroups?.getClosedGroup(destination.publicKey) ?: return@forEach
if (group.adminKey == null) return@forEach Log.w("ConfigurationSync", "Trying to schedule config sync for group we aren't an admin of")
}
val currentStorageJob = storage.getConfigSyncJob(destination)
if (currentStorageJob != null) {
(currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true)
return@publish
}
val newConfigSyncJob = ConfigurationSyncJob(destination)
JobQueue.shared.add(newConfigSyncJob)
}
}
}
@JvmStatic
fun syncConfigurationIfNeeded(context: Context) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
scheduleConfigSync(Destination.Contact(userPublicKey))
}
fun forceSyncConfigurationNowIfNeeded(destination: Destination) {
scheduleConfigSync(destination)
}
fun forceSyncConfigurationNowIfNeeded(context: Context) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Log.e("Loki", NullPointerException("User Public Key is null"))
// Schedule a new job if one doesn't already exist (only)
scheduleConfigSync(Destination.Contact(userPublicKey))
}
private fun maybeUserSecretKey() = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()?.secretKey?.asBytes
fun generateUserProfileConfigDump(): ByteArray? {
val storage = MessagingModuleConfiguration.shared.storage
val ownPublicKey = storage.getUserPublicKey() ?: return null
val config = ConfigurationMessage.getCurrent(listOf()) ?: return null
val secretKey = maybeUserSecretKey() ?: return null
val profile = UserProfile.newInstance(secretKey)
profile.setName(config.displayName)
val picUrl = config.profilePicture
val picKey = config.profileKey
if (!picUrl.isNullOrEmpty() && picKey.isNotEmpty()) {
profile.setPic(UserPic(picUrl, picKey))
}
val ownThreadId = storage.getThreadId(Address.fromSerialized(ownPublicKey))
profile.setNtsPriority(
if (ownThreadId != null)
if (storage.isPinned(ownThreadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
else ConfigBase.PRIORITY_HIDDEN
)
val dump = profile.dump()
profile.free()
return dump
}
fun generateContactConfigDump(): ByteArray? {
val secretKey = maybeUserSecretKey() ?: return null
val storage = MessagingModuleConfiguration.shared.storage
val localUserKey = storage.getUserPublicKey() ?: return null
val contactsWithSettings = storage.getAllContacts().filter { recipient ->
recipient.accountID != localUserKey && recipient.accountID.startsWith(IdPrefix.STANDARD.value)
&& storage.getThreadId(recipient.accountID) != null
}.map { contact ->
val address = Address.fromSerialized(contact.accountID)
val thread = storage.getThreadId(address)
val isPinned = if (thread != null) {
storage.isPinned(thread)
} else false
Triple(contact, storage.getRecipientSettings(address)!!, isPinned)
}
val contactConfig = Contacts.newInstance(secretKey)
for ((contact, settings, isPinned) in contactsWithSettings) {
val url = contact.profilePictureURL
val key = contact.profilePictureEncryptionKey
val userPic = if (url.isNullOrEmpty() || key?.isNotEmpty() != true) {
null
} else {
UserPic(url, key)
}
val contactInfo = Contact(
id = contact.accountID,
name = contact.name.orEmpty(),
nickname = contact.nickname.orEmpty(),
blocked = settings.isBlocked,
approved = settings.isApproved,
approvedMe = settings.hasApprovedMe(),
profilePicture = userPic ?: UserPic.DEFAULT,
priority = if (isPinned) 1 else 0,
expiryMode = if (settings.expireMessages == 0) ExpiryMode.NONE else ExpiryMode.AfterRead(settings.expireMessages.toLong())
)
contactConfig.set(contactInfo)
}
val dump = contactConfig.dump()
contactConfig.free()
if (dump.isEmpty()) return null
return dump
}
fun generateConversationVolatileDump(context: Context): ByteArray? {
val secretKey = maybeUserSecretKey() ?: return null
val storage = MessagingModuleConfiguration.shared.storage
val convoConfig = ConversationVolatileConfig.newInstance(secretKey)
val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.approvedConversationList.use { cursor ->
val reader = threadDb.readerFor(cursor)
var current = reader.next
while (current != null) {
val recipient = current.recipient
val contact = when {
recipient.isCommunityRecipient -> {
val openGroup = storage.getOpenGroup(current.threadId) ?: continue
val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue
convoConfig.getOrConstructCommunity(base, room, pubKey)
}
recipient.isClosedGroupV2Recipient -> {
// It's probably safe to assume there will never be a case where new closed groups will ever be there before a dump is created...
// but just in case...
convoConfig.getOrConstructClosedGroup(recipient.address.serialize())
}
recipient.isLegacyClosedGroupRecipient -> {
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
convoConfig.getOrConstructLegacyGroup(groupPublicKey)
}
recipient.isContactRecipient -> {
if (recipient.isLocalNumber) null // this is handled by the user profile NTS data
else if (recipient.isOpenGroupInboxRecipient) null // specifically exclude
else if (!recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) null
else convoConfig.getOrConstructOneToOne(recipient.address.serialize())
}
else -> null
}
if (contact == null) {
current = reader.next
continue
}
contact.lastRead = current.lastSeen
contact.unread = false
convoConfig.set(contact)
current = reader.next
}
}
val dump = convoConfig.dump()
convoConfig.free()
if (dump.isEmpty()) return null
return dump
}
fun generateUserGroupDump(context: Context): ByteArray? {
val secretKey = maybeUserSecretKey() ?: return null
val storage = MessagingModuleConfiguration.shared.storage
val groupConfig = UserGroupsConfig.newInstance(secretKey)
val allOpenGroups = storage.getAllOpenGroups().values.mapNotNull { openGroup ->
val (baseUrl, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@mapNotNull null
val pubKeyHex = Hex.toStringCondensed(pubKey)
val baseInfo = BaseCommunityInfo(baseUrl, room, pubKeyHex)
val threadId = storage.getThreadId(openGroup) ?: return@mapNotNull null
val isPinned = storage.isPinned(threadId)
GroupInfo.CommunityGroupInfo(baseInfo, if (isPinned) 1 else 0)
}
val allLgc = storage.getAllGroups(includeInactive = false).filter {
it.isLegacyClosedGroup && it.isActive && it.members.size > 1
}.mapNotNull { group ->
val groupAddress = Address.fromSerialized(group.encodedId)
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupAddress.serialize()).toHexString()
val recipient = storage.getRecipientSettings(groupAddress) ?: return@mapNotNull null
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return@mapNotNull null
val threadId = storage.getThreadId(group.encodedId)
val isPinned = threadId?.let { storage.isPinned(threadId) } ?: false
val admins = group.admins.map { it.serialize() to true }.toMap()
val members = group.members.filterNot { it.serialize() !in admins.keys }.map { it.serialize() to false }.toMap()
GroupInfo.LegacyGroupInfo(
accountId = groupPublicKey,
name = group.title,
members = admins + members,
priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = encryptionKeyPair.privateKey.serialize(),
disappearingTimer = recipient.expireMessages.toLong(),
joinedAt = (group.formationTimestamp / 1000L)
)
}
(allOpenGroups + allLgc).forEach { groupInfo ->
groupConfig.set(groupInfo)
}
val dump = groupConfig.dump()
groupConfig.free()
if (dump.isEmpty()) return null
return dump
}
@JvmField
val DELETE_INACTIVE_GROUPS: String = """
DELETE FROM ${GroupDatabase.TABLE_NAME} WHERE ${GroupDatabase.GROUP_ID} IN (SELECT ${ThreadDatabase.ADDRESS} FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} LIKE '${GroupUtil.LEGACY_CLOSED_GROUP_PREFIX}%');

View File

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