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