mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 15:27:46 +00:00
Config revamp
This commit is contained in:
@@ -39,18 +39,17 @@ import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import com.squareup.phrase.Phrase;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.session.libsession.avatars.AvatarHelper;
|
||||
import org.session.libsession.database.MessageDataProvider;
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||
import org.session.libsession.messaging.configs.ConfigSyncHandler;
|
||||
import org.session.libsession.messaging.groups.GroupManagerV2;
|
||||
import org.session.libsession.messaging.groups.RemoveGroupMemberHandler;
|
||||
import org.session.libsession.messaging.notifications.TokenFetcher;
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.LegacyClosedGroupPollerV2;
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||
import org.session.libsession.snode.SnodeModule;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
|
||||
import org.session.libsession.utilities.Device;
|
||||
import org.session.libsession.utilities.Environment;
|
||||
import org.session.libsession.utilities.ProfilePictureUtilities;
|
||||
@@ -94,7 +93,6 @@ import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
|
||||
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
|
||||
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.util.Broadcaster;
|
||||
@@ -104,12 +102,10 @@ import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.PeerConnectionFactory.InitializationOptions;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
@@ -119,11 +115,8 @@ import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.EntryPoints;
|
||||
import dagger.hilt.android.HiltAndroidApp;
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.BuildConfig;
|
||||
import network.loki.messenger.R;
|
||||
import network.loki.messenger.libsession_util.Config;
|
||||
import network.loki.messenger.libsession_util.UserProfile;
|
||||
|
||||
/**
|
||||
* Will be called once when the TextSecure process is created.
|
||||
@@ -169,6 +162,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
@Inject SSKEnvironment.ProfileManagerProtocol profileManager;
|
||||
CallMessageProcessor callMessageProcessor;
|
||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||
@Inject ConfigSyncHandler configSyncHandler;
|
||||
@Inject RemoveGroupMemberHandler removeGroupMemberHandler;
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
|
||||
@@ -272,6 +267,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
|
||||
|
||||
pushRegistrationHandler.run();
|
||||
configSyncHandler.start();
|
||||
removeGroupMemberHandler.start();
|
||||
|
||||
// add our shortcut debug menu if we are not in a release build
|
||||
if (BuildConfig.BUILD_TYPE != "release") {
|
||||
@@ -355,6 +352,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
return typingStatusSender;
|
||||
}
|
||||
|
||||
public TextSecurePreferences getTextSecurePreferences() {
|
||||
return textSecurePreferences;
|
||||
}
|
||||
|
||||
public ReadReceiptManager getReadReceiptManager() {
|
||||
return readReceiptManager;
|
||||
}
|
||||
@@ -444,13 +445,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
|
||||
private static class ProviderInitializationException extends RuntimeException { }
|
||||
private void setUpPollingIfNeeded() {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
String userPublicKey = textSecurePreferences.getLocalNumber();
|
||||
if (userPublicKey == null) return;
|
||||
if (poller != null) {
|
||||
poller.setUserPublicKey(userPublicKey);
|
||||
return;
|
||||
}
|
||||
poller = new Poller(configFactory);
|
||||
poller = new Poller(configFactory, storage, lokiAPIDatabase);
|
||||
}
|
||||
|
||||
public void startPollingIfNeeded() {
|
||||
|
@@ -11,9 +11,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.session.libsession.messaging.groups.GroupManagerV2
|
||||
import org.session.libsession.utilities.AppTextSecurePreferences
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.Toaster
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupManagerV2Impl
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import org.thoughtcrime.securesms.repository.DefaultConversationRepository
|
||||
@@ -35,6 +38,12 @@ abstract class AppModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindProfileManager(profileManager: ProfileManager): SSKEnvironment.ProfileManagerProtocol
|
||||
|
||||
@Binds
|
||||
abstract fun bindConfigFactory(configFactory: ConfigFactory): ConfigFactoryProtocol
|
||||
|
||||
@Binds
|
||||
abstract fun bindLokiAPIDatabaseProtocol(lokiAPIDatabase: LokiAPIDatabase): LokiAPIDatabaseProtocol
|
||||
}
|
||||
|
||||
@Module
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Lazy
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
@@ -16,6 +18,7 @@ import network.loki.messenger.libsession_util.MutableUserProfile
|
||||
import network.loki.messenger.libsession_util.UserGroupsConfig
|
||||
import network.loki.messenger.libsession_util.UserProfile
|
||||
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
|
||||
import network.loki.messenger.libsession_util.util.ConfigPush
|
||||
import network.loki.messenger.libsession_util.util.Contact
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import network.loki.messenger.libsession_util.util.GroupInfo
|
||||
@@ -28,29 +31,39 @@ 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.ConfigMessage
|
||||
import org.session.libsession.utilities.ConfigPushResult
|
||||
import org.session.libsession.utilities.ConfigUpdateNotification
|
||||
import org.session.libsession.utilities.GroupConfigs
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.MutableGroupConfigs
|
||||
import org.session.libsession.utilities.MutableUserConfigs
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.UserConfigType
|
||||
import org.session.libsession.utilities.UserConfigs
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.utilities.AccountId
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.database.ConfigDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
class ConfigFactory(
|
||||
private val context: Context,
|
||||
@Singleton
|
||||
class ConfigFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val configDatabase: ConfigDatabase,
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val storage: StorageProtocol,
|
||||
private val lokiThreadDatabase: LokiThreadDatabase,
|
||||
private val storage: Lazy<StorageProtocol>,
|
||||
private val textSecurePreferences: TextSecurePreferences
|
||||
) : ConfigFactoryProtocol {
|
||||
companion object {
|
||||
// This is a buffer period within which we will process messages which would result in a
|
||||
@@ -182,7 +195,7 @@ class ConfigFactory(
|
||||
members = groupMembers
|
||||
)
|
||||
|
||||
fun persistIfDirty(): Boolean {
|
||||
fun dumpIfNeeded(): Boolean {
|
||||
if (groupInfo.needsDump() || groupMembers.needsDump() || groupKeys.needsDump()) {
|
||||
configDatabase.storeGroupConfigs(
|
||||
publicKey = groupAccountId.hexString,
|
||||
@@ -197,11 +210,10 @@ class ConfigFactory(
|
||||
return false
|
||||
}
|
||||
|
||||
override fun loadKeys(message: ByteArray, hash: String, timestamp: Long): Boolean {
|
||||
return groupKeys.loadKey(message, hash, timestamp, groupInfo.pointer, groupMembers.pointer)
|
||||
}
|
||||
val isDirty: Boolean
|
||||
get() = groupInfo.dirty() || groupMembers.dirty()
|
||||
|
||||
override fun rekeys() {
|
||||
override fun rekey() {
|
||||
groupKeys.rekey(groupInfo.pointer, groupMembers.pointer)
|
||||
}
|
||||
}
|
||||
@@ -211,17 +223,17 @@ class ConfigFactory(
|
||||
|
||||
private val _configUpdateNotifications = MutableSharedFlow<ConfigUpdateNotification>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
onBufferOverflow = BufferOverflow.SUSPEND
|
||||
)
|
||||
override val configUpdateNotifications get() = _configUpdateNotifications
|
||||
|
||||
private fun requiresCurrentUserAccountId(): AccountId =
|
||||
AccountId(requireNotNull(storage.getUserPublicKey()) {
|
||||
AccountId(requireNotNull(textSecurePreferences.getLocalNumber()) {
|
||||
"No logged in user"
|
||||
})
|
||||
|
||||
private fun requiresCurrentUserED25519SecKey(): ByteArray =
|
||||
requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) {
|
||||
requireNotNull(storage.get().getUserED25519KeyPair()?.secretKey?.asBytes) {
|
||||
"No logged in user"
|
||||
}
|
||||
|
||||
@@ -233,7 +245,7 @@ class ConfigFactory(
|
||||
userAccountId,
|
||||
threadDb = threadDb,
|
||||
configDatabase = configDatabase,
|
||||
storage = storage
|
||||
storage = storage.get()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -242,11 +254,16 @@ class ConfigFactory(
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T {
|
||||
/**
|
||||
* Perform an operation on the user configs, and notify listeners if the configs were changed.
|
||||
*
|
||||
* @param cb A function that takes a [UserConfigsImpl] and returns a pair of the result of the operation and a boolean indicating if the configs were changed.
|
||||
*/
|
||||
private fun <T> doWithMutableUserConfigs(cb: (UserConfigsImpl) -> Pair<T, Boolean>): T {
|
||||
return withUserConfigs { configs ->
|
||||
val result = cb(configs as UserConfigsImpl)
|
||||
val (result, changed) = cb(configs as UserConfigsImpl)
|
||||
|
||||
if (configs.persistIfDirty()) {
|
||||
if (changed) {
|
||||
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.UserConfigs)
|
||||
}
|
||||
|
||||
@@ -254,6 +271,32 @@ class ConfigFactory(
|
||||
}
|
||||
}
|
||||
|
||||
override fun mergeUserConfigs(
|
||||
userConfigType: UserConfigType,
|
||||
messages: List<ConfigMessage>
|
||||
) {
|
||||
if (messages.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
return doWithMutableUserConfigs { configs ->
|
||||
val config = when (userConfigType) {
|
||||
UserConfigType.CONTACTS -> configs.contacts
|
||||
UserConfigType.USER_PROFILE -> configs.userProfile
|
||||
UserConfigType.CONVO_INFO_VOLATILE -> configs.convoInfoVolatile
|
||||
UserConfigType.USER_GROUPS -> configs.userGroups
|
||||
}
|
||||
|
||||
Unit to config.merge(messages.map { it.hash to it.data }.toTypedArray()).isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T {
|
||||
return doWithMutableUserConfigs {
|
||||
cb(it) to it.persistIfDirty()
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T {
|
||||
val configs = groupConfigs.getOrPut(groupId) {
|
||||
val groupAdminKey = requireNotNull(withUserConfigs {
|
||||
@@ -275,18 +318,28 @@ class ConfigFactory(
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> doWithMutableGroupConfigs(groupId: AccountId, cb: (GroupConfigsImpl) -> Pair<T, Boolean>): T {
|
||||
return withGroupConfigs(groupId) { configs ->
|
||||
val (result, changed) = cb(configs as GroupConfigsImpl)
|
||||
|
||||
Log.d("ConfigFactory", "Group updated? $groupId: $changed")
|
||||
|
||||
if (changed) {
|
||||
if (!_configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsUpdated(groupId))) {
|
||||
Log.e("ConfigFactory", "Unable to deliver group update notification")
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> withMutableGroupConfigs(
|
||||
groupId: AccountId,
|
||||
cb: (MutableGroupConfigs) -> T
|
||||
): T {
|
||||
return withGroupConfigs(groupId) { configs ->
|
||||
val result = cb(configs as GroupConfigsImpl)
|
||||
|
||||
if (configs.persistIfDirty()) {
|
||||
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsUpdated(groupId))
|
||||
}
|
||||
|
||||
result
|
||||
return doWithMutableGroupConfigs(groupId) {
|
||||
cb(it) to it.dumpIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +362,7 @@ class ConfigFactory(
|
||||
): ByteArray? {
|
||||
return Sodium.decryptForMultipleSimple(
|
||||
encoded = encoded,
|
||||
ed25519SecretKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) {
|
||||
ed25519SecretKey = requireNotNull(storage.get().getUserED25519KeyPair()?.secretKey?.asBytes) {
|
||||
"No logged in user"
|
||||
},
|
||||
domain = domain,
|
||||
@@ -317,18 +370,85 @@ class ConfigFactory(
|
||||
)
|
||||
}
|
||||
|
||||
override fun mergeGroupConfigMessages(
|
||||
groupId: AccountId,
|
||||
keys: List<ConfigMessage>,
|
||||
info: List<ConfigMessage>,
|
||||
members: List<ConfigMessage>
|
||||
) {
|
||||
doWithMutableGroupConfigs(groupId) { configs ->
|
||||
// Keys must be loaded first as they are used to decrypt the other config messages
|
||||
val keysLoaded = keys.fold(false) { acc, msg ->
|
||||
configs.groupKeys.loadKey(msg.data, msg.hash, msg.timestamp, configs.groupInfo.pointer, configs.groupMembers.pointer) || acc
|
||||
}
|
||||
|
||||
val infoMerged = info.isNotEmpty() &&
|
||||
configs.groupInfo.merge(info.map { it.hash to it.data }.toTypedArray()).isNotEmpty()
|
||||
|
||||
val membersMerged = members.isNotEmpty() &&
|
||||
configs.groupMembers.merge(members.map { it.hash to it.data }.toTypedArray()).isNotEmpty()
|
||||
|
||||
configs.dumpIfNeeded()
|
||||
|
||||
Unit to (keysLoaded || infoMerged || membersMerged)
|
||||
}
|
||||
}
|
||||
|
||||
override fun confirmUserConfigsPushed(
|
||||
contacts: Pair<ConfigPush, ConfigPushResult>?,
|
||||
userProfile: Pair<ConfigPush, ConfigPushResult>?,
|
||||
convoInfoVolatile: Pair<ConfigPush, ConfigPushResult>?,
|
||||
userGroups: Pair<ConfigPush, ConfigPushResult>?
|
||||
) {
|
||||
if (contacts == null && userProfile == null && convoInfoVolatile == null && userGroups == null) {
|
||||
return
|
||||
}
|
||||
|
||||
doWithMutableUserConfigs { configs ->
|
||||
contacts?.let { (push, result) -> configs.contacts.confirmPushed(push.seqNo, result.hash) }
|
||||
userProfile?.let { (push, result) -> configs.userProfile.confirmPushed(push.seqNo, result.hash) }
|
||||
convoInfoVolatile?.let { (push, result) -> configs.convoInfoVolatile.confirmPushed(push.seqNo, result.hash) }
|
||||
userGroups?.let { (push, result) -> configs.userGroups.confirmPushed(push.seqNo, result.hash) }
|
||||
|
||||
Unit to configs.persistIfDirty()
|
||||
}
|
||||
}
|
||||
|
||||
override fun confirmGroupConfigsPushed(
|
||||
groupId: AccountId,
|
||||
members: Pair<ConfigPush, ConfigPushResult>?,
|
||||
info: Pair<ConfigPush, ConfigPushResult>?,
|
||||
keysPush: ConfigPushResult?
|
||||
) {
|
||||
if (members == null && info == null && keysPush == null) {
|
||||
return
|
||||
}
|
||||
|
||||
doWithMutableGroupConfigs(groupId) { configs ->
|
||||
members?.let { (push, result) -> configs.groupMembers.confirmPushed(push.seqNo, result.hash) }
|
||||
info?.let { (push, result) -> configs.groupInfo.confirmPushed(push.seqNo, result.hash) }
|
||||
keysPush?.let { (hash, timestamp) ->
|
||||
val pendingConfig = configs.groupKeys.pendingConfig()
|
||||
if (pendingConfig != null) {
|
||||
configs.groupKeys.loadKey(pendingConfig, hash, timestamp, configs.groupInfo.pointer, configs.groupMembers.pointer)
|
||||
}
|
||||
}
|
||||
|
||||
Unit to configs.dumpIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun conversationInConfig(
|
||||
publicKey: String?,
|
||||
groupPublicKey: String?,
|
||||
openGroupId: String?,
|
||||
visibleOnly: Boolean
|
||||
): Boolean {
|
||||
val userPublicKey = storage.getUserPublicKey() ?: return false
|
||||
val userPublicKey = storage.get().getUserPublicKey() ?: return false
|
||||
|
||||
if (openGroupId != null) {
|
||||
val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context)
|
||||
val openGroup =
|
||||
get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false
|
||||
val openGroup = lokiThreadDatabase.getOpenGroupChat(threadId) ?: return false
|
||||
|
||||
// Not handling the `hidden` behaviour for communities so just indicate the existence
|
||||
return withUserConfigs {
|
||||
|
@@ -7,6 +7,7 @@ import network.loki.messenger.libsession_util.util.GroupInfo
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.messaging.groups.GroupManagerV2
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.AccountId
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@@ -15,7 +16,8 @@ class PollerFactory(
|
||||
private val executor: CoroutineDispatcher,
|
||||
private val configFactory: ConfigFactory,
|
||||
private val groupManagerV2: Lazy<GroupManagerV2>,
|
||||
private val storage: StorageProtocol,
|
||||
private val storage: Lazy<StorageProtocol>,
|
||||
private val lokiApiDatabase: LokiAPIDatabaseProtocol,
|
||||
) {
|
||||
|
||||
private val pollers = ConcurrentHashMap<AccountId, ClosedGroupPoller>()
|
||||
@@ -29,7 +31,15 @@ class PollerFactory(
|
||||
if (invited != false) return null
|
||||
|
||||
return pollers.getOrPut(sessionId) {
|
||||
ClosedGroupPoller(scope, executor, sessionId, configFactory, groupManagerV2.get(), storage)
|
||||
ClosedGroupPoller(
|
||||
scope = scope,
|
||||
executor = executor,
|
||||
closedGroupSessionId = sessionId,
|
||||
configFactoryProtocol = configFactory,
|
||||
groupManagerV2 = groupManagerV2.get(),
|
||||
storage = storage.get(),
|
||||
lokiApiDatabase = lokiApiDatabase,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -14,6 +14,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.messaging.groups.GroupManagerV2
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.thoughtcrime.securesms.database.ConfigDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import javax.inject.Named
|
||||
@@ -26,15 +28,6 @@ object SessionUtilModule {
|
||||
|
||||
private const val POLLER_SCOPE = "poller_coroutine_scope"
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConfigFactory(
|
||||
@ApplicationContext context: Context,
|
||||
configDatabase: ConfigDatabase,
|
||||
storageProtocol: StorageProtocol,
|
||||
threadDatabase: ThreadDatabase,
|
||||
): ConfigFactory = ConfigFactory(context, configDatabase, threadDatabase, storageProtocol)
|
||||
|
||||
@Provides
|
||||
@Named(POLLER_SCOPE)
|
||||
fun providePollerScope(): CoroutineScope = GlobalScope
|
||||
@@ -49,12 +42,14 @@ object SessionUtilModule {
|
||||
fun providePollerFactory(@Named(POLLER_SCOPE) coroutineScope: CoroutineScope,
|
||||
@Named(POLLER_SCOPE) dispatcher: CoroutineDispatcher,
|
||||
configFactory: ConfigFactory,
|
||||
storage: StorageProtocol,
|
||||
groupManagerV2: Lazy<GroupManagerV2>) = PollerFactory(
|
||||
storage: Lazy<StorageProtocol>,
|
||||
groupManagerV2: Lazy<GroupManagerV2>,
|
||||
lokiApiDatabase: LokiAPIDatabaseProtocol) = PollerFactory(
|
||||
scope = coroutineScope,
|
||||
executor = dispatcher,
|
||||
configFactory = configFactory,
|
||||
groupManagerV2 = groupManagerV2,
|
||||
storage = storage
|
||||
storage = storage,
|
||||
lokiApiDatabase = lokiApiDatabase,
|
||||
)
|
||||
}
|
@@ -7,7 +7,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.thoughtcrime.securesms.conversation.start.NullStartConversationDelegate
|
||||
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
|
@@ -5,9 +5,11 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.groups.compose.EditGroupScreen
|
||||
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EditGroupActivity: PassphraseRequiredActionBarActivity() {
|
||||
|
@@ -8,6 +8,7 @@ import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -44,7 +45,7 @@ class EditGroupViewModel @AssistedInject constructor(
|
||||
|
||||
// Output: the source-of-truth group information. Other states are derived from this.
|
||||
private val groupInfo: StateFlow<Pair<GroupDisplayInfo, List<GroupMemberState>>?> =
|
||||
configFactory.configUpdateNotifications
|
||||
(configFactory.configUpdateNotifications as Flow<Any>)
|
||||
.onStart { emit(Unit) }
|
||||
.map {
|
||||
withContext(Dispatchers.Default) {
|
||||
|
@@ -3,19 +3,14 @@ package org.thoughtcrime.securesms.groups
|
||||
import android.content.Context
|
||||
import com.google.protobuf.ByteString
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE
|
||||
import network.loki.messenger.libsession_util.GroupInfoConfig
|
||||
import network.loki.messenger.libsession_util.GroupKeysConfig
|
||||
import network.loki.messenger.libsession_util.GroupMembersConfig
|
||||
import network.loki.messenger.libsession_util.UserGroupsConfig
|
||||
import network.loki.messenger.libsession_util.util.Conversation
|
||||
import network.loki.messenger.libsession_util.util.GroupInfo
|
||||
import network.loki.messenger.libsession_util.util.GroupMember
|
||||
import network.loki.messenger.libsession_util.util.INVITE_STATUS_FAILED
|
||||
import network.loki.messenger.libsession_util.util.INVITE_STATUS_SENT
|
||||
import network.loki.messenger.libsession_util.util.Sodium
|
||||
import network.loki.messenger.libsession_util.util.UserPic
|
||||
@@ -24,7 +19,6 @@ import org.session.libsession.database.userAuth
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.groups.GroupManagerV2
|
||||
import org.session.libsession.messaging.jobs.ConfigurationSyncJob.Companion.messageInformation
|
||||
import org.session.libsession.messaging.jobs.InviteContactsJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
@@ -39,12 +33,11 @@ import org.session.libsession.snode.OwnedSwarmAuth
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.snode.model.BatchResponse
|
||||
import org.session.libsession.snode.model.StoreMessageResponse
|
||||
import org.session.libsession.snode.utilities.await
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.getClosedGroup
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.withGroupConfigsOrNull
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
|
||||
@@ -60,7 +53,6 @@ import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.dependencies.PollerFactory
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -97,8 +89,6 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
): Recipient = withContext(dispatcher) {
|
||||
val ourAccountId =
|
||||
requireNotNull(storage.getUserPublicKey()) { "Our account ID is not available" }
|
||||
val ourKeys =
|
||||
requireNotNull(storage.getUserED25519KeyPair()) { "Our ED25519 key pair is not available" }
|
||||
val ourProfile = storage.getUserProfile()
|
||||
|
||||
val groupCreationTimestamp = SnodeAPI.nowWithOffset
|
||||
@@ -108,9 +98,8 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
configs.userGroups.createGroup().also(configs.userGroups::set)
|
||||
}
|
||||
|
||||
val adminKey = checkNotNull(group.adminKey) { "Admin key is null for new group creation." }
|
||||
checkNotNull(group.adminKey) { "Admin key is null for new group creation." }
|
||||
val groupId = group.groupAccountId
|
||||
val groupAuth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey)
|
||||
|
||||
try {
|
||||
configFactory.withMutableGroupConfigs(groupId) { configs ->
|
||||
@@ -141,7 +130,7 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
)
|
||||
|
||||
// Manually re-key to prevent issue with linked admin devices
|
||||
configs.rekeys()
|
||||
configs.rekey()
|
||||
}
|
||||
|
||||
configFactory.withMutableUserConfigs {
|
||||
@@ -238,10 +227,10 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
)
|
||||
)
|
||||
} else {
|
||||
configs.rekeys()
|
||||
configs.rekey()
|
||||
}
|
||||
|
||||
newMembers.map { configs.groupKeys.makeSubAccount(group) }
|
||||
newMembers.map { configs.groupKeys.getSubAccountToken(it) }
|
||||
}
|
||||
|
||||
|
||||
@@ -301,9 +290,10 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun handleMemberLeft(message: GroupUpdated, closedGroupId: AccountId) {
|
||||
val userGroups = configFactory.userGroups ?: return
|
||||
val closedGroupHexString = closedGroupId.hexString
|
||||
val closedGroup = userGroups.getClosedGroup(closedGroupId.hexString) ?: return
|
||||
val closedGroup =
|
||||
configFactory.withUserConfigs { it.userGroups.getClosedGroup(closedGroupId.hexString) }
|
||||
?: return
|
||||
if (closedGroup.hasAdminKey()) {
|
||||
// re-key and do a new config removing the previous member
|
||||
doRemoveMembers(
|
||||
@@ -313,22 +303,27 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
removeMemberMessages = false
|
||||
)
|
||||
} else {
|
||||
configFactory.getGroupMemberConfig(closedGroupId)?.use { memberConfig ->
|
||||
// if the leaving member is an admin, disable the group and remove it
|
||||
// This is just to emulate the "existing" group behaviour, this will need to be removed in future
|
||||
if (memberConfig.get(message.sender!!)?.admin == true) {
|
||||
pollerFactory.pollerFor(closedGroupId)?.stop()
|
||||
storage.getThreadId(Address.fromSerialized(closedGroupHexString))
|
||||
?.let(storage::deleteConversation)
|
||||
configFactory.removeGroup(closedGroupId)
|
||||
}
|
||||
val hasAnyAdminRemaining = configFactory.withGroupConfigs(closedGroupId) { configs ->
|
||||
configs.groupMembers.all()
|
||||
.asSequence()
|
||||
.filterNot { it.sessionId == message.sender }
|
||||
.any { it.admin && !it.removed }
|
||||
}
|
||||
|
||||
// if the leaving member is an admin, disable the group and remove it
|
||||
// This is just to emulate the "existing" group behaviour, this will need to be removed in future
|
||||
if (!hasAnyAdminRemaining) {
|
||||
pollerFactory.pollerFor(closedGroupId)?.stop()
|
||||
storage.getThreadId(Address.fromSerialized(closedGroupHexString))
|
||||
?.let(storage::deleteConversation)
|
||||
configFactory.removeGroup(closedGroupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun leaveGroup(group: AccountId, deleteOnLeave: Boolean) {
|
||||
val canSendGroupMessage =
|
||||
configFactory.userGroups?.getClosedGroup(group.hexString)?.kicked != true
|
||||
configFactory.withUserConfigs { it.userGroups.getClosedGroup(group.hexString) }?.kicked != true
|
||||
val address = Address.fromSerialized(group.hexString)
|
||||
|
||||
if (canSendGroupMessage) {
|
||||
@@ -358,79 +353,81 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
if (deleteOnLeave) {
|
||||
storage.getThreadId(address)?.let(storage::deleteConversation)
|
||||
configFactory.removeGroup(group)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun promoteMember(group: AccountId, members: List<AccountId>): Unit =
|
||||
withContext(dispatcher) {
|
||||
val adminKey = requireAdminAccess(group)
|
||||
override suspend fun promoteMember(
|
||||
group: AccountId,
|
||||
members: List<AccountId>
|
||||
): Unit = withContext(dispatcher) {
|
||||
val adminKey = requireAdminAccess(group)
|
||||
val groupName = configFactory.withGroupConfigs(group) { it.groupInfo.getName() }
|
||||
|
||||
configFactory.withGroupConfigsOrNull(group) { info, membersConfig, keys ->
|
||||
// Promote the members by sending a message containing the admin key to each member's swarm,
|
||||
// we do this concurrently and then update the group configs after all the messages are sent.
|
||||
val promoteResult = members.asSequence()
|
||||
.mapNotNull { membersConfig.get(it.hexString) }
|
||||
.map { memberConfig ->
|
||||
async {
|
||||
val message = GroupUpdated(
|
||||
GroupUpdateMessage.newBuilder()
|
||||
.setPromoteMessage(
|
||||
DataMessage.GroupUpdatePromoteMessage.newBuilder()
|
||||
.setGroupIdentitySeed(ByteString.copyFrom(adminKey))
|
||||
.setName(info.getName())
|
||||
)
|
||||
.build()
|
||||
)
|
||||
// Send out the promote message to the members concurrently
|
||||
val promotionDeferred = members.associateWith { member ->
|
||||
async {
|
||||
val message = GroupUpdated(
|
||||
GroupUpdateMessage.newBuilder()
|
||||
.setPromoteMessage(
|
||||
DataMessage.GroupUpdatePromoteMessage.newBuilder()
|
||||
.setGroupIdentitySeed(ByteString.copyFrom(adminKey))
|
||||
.setName(groupName)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
try {
|
||||
MessageSender.sendNonDurably(
|
||||
message = message,
|
||||
address = Address.fromSerialized(memberConfig.sessionId),
|
||||
isSyncMessage = false
|
||||
).await()
|
||||
|
||||
memberConfig.setPromoteSent()
|
||||
} catch (ec: Exception) {
|
||||
Log.e(TAG, "Failed to send promote message", ec)
|
||||
memberConfig.setPromoteFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
|
||||
for (result in promoteResult) {
|
||||
membersConfig.set(result.await())
|
||||
}
|
||||
|
||||
configFactory.saveGroupConfigs(keys, info, membersConfig)
|
||||
MessageSender.sendNonDurably(
|
||||
message = message,
|
||||
address = Address.fromSerialized(member.hexString),
|
||||
isSyncMessage = false
|
||||
).await()
|
||||
}
|
||||
|
||||
// Send a group update message to the group telling members someone has been promoted
|
||||
val groupDestination = Destination.ClosedGroup(group.hexString)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination)
|
||||
val timestamp = SnodeAPI.nowWithOffset
|
||||
val signature = SodiumUtilities.sign(
|
||||
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.PROMOTED, timestamp),
|
||||
adminKey
|
||||
)
|
||||
val message = GroupUpdated(
|
||||
GroupUpdateMessage.newBuilder()
|
||||
.setMemberChangeMessage(
|
||||
GroupUpdateMemberChangeMessage.newBuilder()
|
||||
.addAllMemberSessionIds(members.map { it.hexString })
|
||||
.setType(GroupUpdateMemberChangeMessage.Type.PROMOTED)
|
||||
.setAdminSignature(ByteString.copyFrom(signature))
|
||||
)
|
||||
.build()
|
||||
).apply {
|
||||
sentTimestamp = timestamp
|
||||
}
|
||||
|
||||
MessageSender.send(message, Address.fromSerialized(group.hexString))
|
||||
storage.insertGroupInfoChange(message, group)
|
||||
}
|
||||
|
||||
// Wait and gather all the promote message sending result into a result map
|
||||
val promotedByMemberIDs = promotionDeferred
|
||||
.mapValues {
|
||||
runCatching { it.value.await() }.isSuccess
|
||||
}
|
||||
|
||||
// Update each member's status
|
||||
configFactory.withMutableGroupConfigs(group) { configs ->
|
||||
promotedByMemberIDs.asSequence()
|
||||
.mapNotNull { (member, success) ->
|
||||
configs.groupMembers.get(member.hexString)?.copy(
|
||||
promotionStatus = if (success) {
|
||||
INVITE_STATUS_SENT
|
||||
} else {
|
||||
INVITE_STATUS_FAILED
|
||||
}
|
||||
)
|
||||
}
|
||||
.forEach(configs.groupMembers::set)
|
||||
}
|
||||
|
||||
// Send a group update message to the group telling members someone has been promoted
|
||||
val timestamp = SnodeAPI.nowWithOffset
|
||||
val signature = SodiumUtilities.sign(
|
||||
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.PROMOTED, timestamp),
|
||||
adminKey
|
||||
)
|
||||
val message = GroupUpdated(
|
||||
GroupUpdateMessage.newBuilder()
|
||||
.setMemberChangeMessage(
|
||||
GroupUpdateMemberChangeMessage.newBuilder()
|
||||
.addAllMemberSessionIds(members.map { it.hexString })
|
||||
.setType(GroupUpdateMemberChangeMessage.Type.PROMOTED)
|
||||
.setAdminSignature(ByteString.copyFrom(signature))
|
||||
)
|
||||
.build()
|
||||
).apply {
|
||||
sentTimestamp = timestamp
|
||||
}
|
||||
|
||||
MessageSender.send(message, Address.fromSerialized(group.hexString))
|
||||
storage.insertGroupInfoChange(message, group)
|
||||
}
|
||||
|
||||
private suspend fun doRemoveMembers(
|
||||
group: AccountId,
|
||||
removedMembers: List<AccountId>,
|
||||
@@ -440,26 +437,27 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
val adminKey = requireAdminAccess(group)
|
||||
val groupAuth = OwnedSwarmAuth.ofClosedGroup(group, adminKey)
|
||||
|
||||
configFactory.withGroupConfigsOrNull(group) { info, members, keys ->
|
||||
// To remove a member from a group, we need to first:
|
||||
// 1. Notify the swarm that this member's key has bene revoked
|
||||
// 2. Send a "kicked" message to a special namespace that the kicked member can still read
|
||||
// 3. Optionally, send "delete member messages" to the group. (So that every device in the group
|
||||
// delete this member's messages locally.)
|
||||
// These three steps will be included in a sequential call as they all need to be done in order.
|
||||
// After these steps are all done, we will do the following:
|
||||
// Update the group configs to remove the member, sync if needed, then
|
||||
// delete the member's messages locally and remotely.
|
||||
// To remove a member from a group, we need to first:
|
||||
// 1. Notify the swarm that this member's key has bene revoked
|
||||
// 2. Send a "kicked" message to a special namespace that the kicked member can still read
|
||||
// 3. Optionally, send "delete member messages" to the group. (So that every device in the group
|
||||
// delete this member's messages locally.)
|
||||
// These three steps will be included in a sequential call as they all need to be done in order.
|
||||
// After these steps are all done, we will do the following:
|
||||
// Update the group configs to remove the member, sync if needed, then
|
||||
// delete the member's messages locally and remotely.
|
||||
|
||||
val essentialRequests = configFactory.withGroupConfigs(group) { configs ->
|
||||
val messageSendTimestamp = SnodeAPI.nowWithOffset
|
||||
|
||||
val essentialRequests = buildList {
|
||||
buildList {
|
||||
this += SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest(
|
||||
groupAdminAuth = groupAuth,
|
||||
subAccountTokens = removedMembers.map(keys::getSubAccountToken)
|
||||
subAccountTokens = removedMembers.map(configs.groupKeys::getSubAccountToken)
|
||||
)
|
||||
|
||||
this += Sodium.encryptForMultipleSimple(
|
||||
messages = removedMembers.map { "${it.hexString}-${keys.currentGeneration()}".encodeToByteArray() }
|
||||
messages = removedMembers.map { "${it.hexString}-${configs.groupKeys.currentGeneration()}".encodeToByteArray() }
|
||||
.toTypedArray(),
|
||||
recipients = removedMembers.map { it.pubKeyBytes }.toTypedArray(),
|
||||
ed25519SecretKey = adminKey,
|
||||
@@ -506,132 +504,97 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val snode = SnodeAPI.getSingleTargetSnode(group.hexString).await()
|
||||
val responses = SnodeAPI.getBatchResponse(
|
||||
snode,
|
||||
group.hexString,
|
||||
essentialRequests,
|
||||
sequence = true
|
||||
)
|
||||
val snode = SnodeAPI.getSingleTargetSnode(group.hexString).await()
|
||||
val responses = SnodeAPI.getBatchResponse(
|
||||
snode,
|
||||
group.hexString,
|
||||
essentialRequests,
|
||||
sequence = true
|
||||
)
|
||||
|
||||
responses.requireAllRequestsSuccessful("Failed to execute essential steps for removing member")
|
||||
responses.requireAllRequestsSuccessful("Failed to execute essential steps for removing member")
|
||||
|
||||
// Next step: update group configs, rekey, remove member messages if required
|
||||
val messagesToDelete = mutableListOf<String>()
|
||||
for (member in removedMembers) {
|
||||
members.erase(member.hexString)
|
||||
}
|
||||
// Next step: update group configs, rekey, remove member messages if required
|
||||
configFactory.withMutableGroupConfigs(group) { configs ->
|
||||
removedMembers.forEach { configs.groupMembers.erase(it.hexString) }
|
||||
configs.rekey()
|
||||
}
|
||||
|
||||
keys.rekey(info, members)
|
||||
|
||||
if (removeMemberMessages) {
|
||||
val threadId = storage.getThreadId(Address.fromSerialized(group.hexString))
|
||||
if (threadId != null) {
|
||||
for (member in removedMembers) {
|
||||
for (msg in mmsSmsDatabase.getUserMessages(threadId, member.hexString)) {
|
||||
val serverHash = lokiDatabase.getMessageServerHash(msg.id, msg.isMms)
|
||||
if (serverHash != null) {
|
||||
messagesToDelete.add(serverHash)
|
||||
}
|
||||
if (removeMemberMessages) {
|
||||
val threadId = storage.getThreadId(Address.fromSerialized(group.hexString))
|
||||
if (threadId != null) {
|
||||
val messagesToDelete = mutableListOf<String>()
|
||||
for (member in removedMembers) {
|
||||
for (msg in mmsSmsDatabase.getUserMessages(threadId, member.hexString)) {
|
||||
val serverHash = lokiDatabase.getMessageServerHash(msg.id, msg.isMms)
|
||||
if (serverHash != null) {
|
||||
messagesToDelete.add(serverHash)
|
||||
}
|
||||
|
||||
storage.deleteMessagesByUser(threadId, member.hexString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val requests = buildList {
|
||||
keys.messageInformation(groupAuth)?.let {
|
||||
this += "Sync keys config messages" to it.batch
|
||||
storage.deleteMessagesByUser(threadId, member.hexString)
|
||||
}
|
||||
|
||||
this += "Sync info config messages" to info.messageInformation(
|
||||
messagesToDelete,
|
||||
groupAuth
|
||||
).batch
|
||||
|
||||
this += "Sync member config messages" to members.messageInformation(
|
||||
messagesToDelete,
|
||||
groupAuth
|
||||
).batch
|
||||
|
||||
this += "Delete outdated config and member messages" to SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||
groupAuth,
|
||||
messagesToDelete
|
||||
)
|
||||
}
|
||||
|
||||
val response = SnodeAPI.getBatchResponse(
|
||||
snode = snode,
|
||||
publicKey = group.hexString,
|
||||
requests = requests.map { it.second }
|
||||
)
|
||||
|
||||
response.requireAllRequestsSuccessful("Failed to remove members")
|
||||
|
||||
// Persist the changes
|
||||
configFactory.saveGroupConfigs(keys, info, members)
|
||||
|
||||
if (sendRemovedMessage) {
|
||||
val timestamp = messageSendTimestamp
|
||||
val signature = SodiumUtilities.sign(
|
||||
buildMemberChangeSignature(
|
||||
GroupUpdateMemberChangeMessage.Type.REMOVED,
|
||||
timestamp
|
||||
),
|
||||
adminKey
|
||||
)
|
||||
|
||||
val updateMessage = GroupUpdateMessage.newBuilder()
|
||||
.setMemberChangeMessage(
|
||||
GroupUpdateMemberChangeMessage.newBuilder()
|
||||
.addAllMemberSessionIds(removedMembers.map { it.hexString })
|
||||
.setType(GroupUpdateMemberChangeMessage.Type.REMOVED)
|
||||
.setAdminSignature(ByteString.copyFrom(signature))
|
||||
SnodeAPI.sendBatchRequest(
|
||||
snode, group.hexString, SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||
groupAuth,
|
||||
messagesToDelete
|
||||
)
|
||||
.build()
|
||||
val message = GroupUpdated(
|
||||
updateMessage
|
||||
).apply { sentTimestamp = timestamp }
|
||||
MessageSender.send(message, Destination.ClosedGroup(group.hexString), false)
|
||||
storage.insertGroupInfoChange(message, group)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
|
||||
Destination.ClosedGroup(group.hexString)
|
||||
)
|
||||
if (sendRemovedMessage) {
|
||||
val timestamp = SnodeAPI.nowWithOffset
|
||||
val signature = SodiumUtilities.sign(
|
||||
buildMemberChangeSignature(
|
||||
GroupUpdateMemberChangeMessage.Type.REMOVED,
|
||||
timestamp
|
||||
),
|
||||
adminKey
|
||||
)
|
||||
|
||||
val updateMessage = GroupUpdateMessage.newBuilder()
|
||||
.setMemberChangeMessage(
|
||||
GroupUpdateMemberChangeMessage.newBuilder()
|
||||
.addAllMemberSessionIds(removedMembers.map { it.hexString })
|
||||
.setType(GroupUpdateMemberChangeMessage.Type.REMOVED)
|
||||
.setAdminSignature(ByteString.copyFrom(signature))
|
||||
)
|
||||
.build()
|
||||
val message = GroupUpdated(
|
||||
updateMessage
|
||||
).apply { sentTimestamp = timestamp }
|
||||
MessageSender.send(message, Destination.ClosedGroup(group.hexString), false)
|
||||
storage.insertGroupInfoChange(message, group)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun respondToInvitation(groupId: AccountId, approved: Boolean) =
|
||||
withContext(dispatcher) {
|
||||
val groups = requireNotNull(configFactory.userGroups) {
|
||||
"User groups config is not available"
|
||||
}
|
||||
val group = requireNotNull(
|
||||
configFactory.withUserConfigs { it.userGroups.getClosedGroup(groupId.hexString) }
|
||||
) { "User groups config is not available" }
|
||||
|
||||
val threadId =
|
||||
checkNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) {
|
||||
"No thread has been created for the group"
|
||||
}
|
||||
|
||||
val group = requireNotNull(groups.getClosedGroup(groupId.hexString)) {
|
||||
"Group must have been created into the config object before responding to an invitation"
|
||||
}
|
||||
|
||||
// Whether approved or not, delete the invite
|
||||
lokiDatabase.deleteGroupInviteReferrer(threadId)
|
||||
|
||||
if (approved) {
|
||||
approveGroupInvite(groups, group, threadId)
|
||||
approveGroupInvite(group, threadId)
|
||||
} else {
|
||||
groups.eraseClosedGroup(groupId.hexString)
|
||||
configFactory.withMutableUserConfigs { it.userGroups.eraseClosedGroup(groupId.hexString) }
|
||||
storage.deleteConversation(threadId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun approveGroupInvite(
|
||||
groups: UserGroupsConfig,
|
||||
group: GroupInfo.ClosedGroupInfo,
|
||||
threadId: Long,
|
||||
) {
|
||||
@@ -640,9 +603,9 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
}
|
||||
|
||||
// Clear the invited flag of the group in the config
|
||||
groups.set(group.copy(invited = false))
|
||||
configFactory.persist(forConfigObject = groups, timestamp = SnodeAPI.nowWithOffset)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application)
|
||||
configFactory.withMutableUserConfigs { configs ->
|
||||
configs.userGroups.set(group.copy(invited = false))
|
||||
}
|
||||
|
||||
if (group.adminKey == null) {
|
||||
// Send an invite response to the group if we are invited as a regular member
|
||||
@@ -659,19 +622,13 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
)
|
||||
} else {
|
||||
// If we are invited as admin, we can just update the group info ourselves
|
||||
configFactory.withGroupConfigsOrNull(group.groupAccountId) { info, members, keys ->
|
||||
members.get(key)?.let { member ->
|
||||
members.set(member.setPromoteSuccess().setAccepted())
|
||||
|
||||
configFactory.saveGroupConfigs(keys, info, members)
|
||||
configFactory.withMutableGroupConfigs(group.groupAccountId) { configs ->
|
||||
configs.groupMembers.get(key)?.let { member ->
|
||||
configs.groupMembers.set(member.setPromoteSuccess().setAccepted())
|
||||
}
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
|
||||
destination = Destination.ClosedGroup(group.groupAccountId.hexString)
|
||||
)
|
||||
}
|
||||
|
||||
pollerFactory.pollerFor(group.groupAccountId)?.start()
|
||||
@@ -696,8 +653,12 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
if (inviteMessageHash != null) {
|
||||
val auth = requireNotNull(storage.userAuth) { "No current user available" }
|
||||
SnodeAPI.sendBatchRequest(
|
||||
auth.accountId,
|
||||
SnodeAPI.buildAuthenticatedDeleteBatchInfo(auth, listOf(inviteMessageHash)),
|
||||
snode = SnodeAPI.getSingleTargetSnode(groupId.hexString).await(),
|
||||
publicKey = auth.accountId.hexString,
|
||||
request = SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||
auth,
|
||||
listOf(inviteMessageHash)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -709,12 +670,9 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
promoter: AccountId,
|
||||
promoteMessageHash: String?
|
||||
) = withContext(dispatcher) {
|
||||
val groups = requireNotNull(configFactory.userGroups) {
|
||||
"User groups config is not available"
|
||||
}
|
||||
|
||||
val userAuth = requireNotNull(storage.userAuth) { "No current user available" }
|
||||
var group = groups.getClosedGroup(groupId.hexString)
|
||||
val group =
|
||||
configFactory.withUserConfigs { it.userGroups.getClosedGroup(groupId.hexString) }
|
||||
|
||||
if (group == null) {
|
||||
// If we haven't got the group in the config, it could mean that we haven't
|
||||
@@ -729,34 +687,27 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
)
|
||||
} else {
|
||||
// If we have the group in the config, we can just update the admin key
|
||||
group = group.copy(adminKey = adminKey)
|
||||
groups.set(group)
|
||||
configFactory.persist(groups, SnodeAPI.nowWithOffset)
|
||||
configFactory.withMutableUserConfigs {
|
||||
it.userGroups.set(group.copy(adminKey = adminKey))
|
||||
}
|
||||
|
||||
// Update our promote state
|
||||
configFactory.withGroupConfigsOrNull(groupId) { info, members, keys ->
|
||||
members.get(userAuth.accountId.hexString)?.let { member ->
|
||||
members.set(member.setPromoteSuccess())
|
||||
|
||||
configFactory.saveGroupConfigs(keys, info, members)
|
||||
configFactory.withMutableGroupConfigs(groupId) { configs ->
|
||||
configs.groupMembers.get(userAuth.accountId.hexString)?.let { member ->
|
||||
configs.groupMembers.set(member.setPromoteSuccess())
|
||||
}
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
|
||||
destination = Destination.ClosedGroup(groupId.hexString)
|
||||
)
|
||||
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application)
|
||||
}
|
||||
|
||||
// Delete the promotion message remotely
|
||||
if (promoteMessageHash != null) {
|
||||
SnodeAPI.sendBatchRequest(
|
||||
userAuth.accountId,
|
||||
SnodeAPI.buildAuthenticatedDeleteBatchInfo(userAuth, listOf(promoteMessageHash)),
|
||||
)
|
||||
SnodeAPI.deleteMessage(
|
||||
userAuth.accountId.hexString,
|
||||
userAuth,
|
||||
listOf(promoteMessageHash)
|
||||
).await()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -777,12 +728,8 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
fromPromotion: Boolean,
|
||||
inviter: AccountId,
|
||||
) {
|
||||
val groups = requireNotNull(configFactory.userGroups) {
|
||||
"User groups config is not available"
|
||||
}
|
||||
|
||||
// If we have already received an invitation in the past, we should not process this one
|
||||
if (groups.getClosedGroup(groupId.hexString)?.invited == true) {
|
||||
if (configFactory.withUserConfigs { it.userGroups.getClosedGroup(groupId.hexString) }?.invited == true) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -799,15 +746,17 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
invited = !shouldAutoApprove,
|
||||
name = groupName,
|
||||
)
|
||||
groups.set(closedGroupInfo)
|
||||
|
||||
configFactory.persist(groups, SnodeAPI.nowWithOffset)
|
||||
configFactory.withMutableUserConfigs {
|
||||
it.userGroups.set(closedGroupInfo)
|
||||
}
|
||||
|
||||
profileManager.setName(application, recipient, groupName)
|
||||
val groupThreadId = storage.getOrCreateThreadIdFor(recipient.address)
|
||||
storage.setRecipientApprovedMe(recipient, true)
|
||||
storage.setRecipientApproved(recipient, shouldAutoApprove)
|
||||
if (shouldAutoApprove) {
|
||||
approveGroupInvite(groups, closedGroupInfo, groupThreadId)
|
||||
approveGroupInvite(closedGroupInfo, groupThreadId)
|
||||
} else {
|
||||
lokiDatabase.addGroupInviteReferrer(groupThreadId, inviter.hexString)
|
||||
storage.insertGroupInviteControlMessage(
|
||||
@@ -829,28 +778,18 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val groups = requireNotNull(configFactory.userGroups) {
|
||||
"User groups config is not available"
|
||||
}
|
||||
|
||||
val adminKey = groups.getClosedGroup(groupId.hexString)?.adminKey
|
||||
val adminKey = configFactory.getClosedGroup(groupId)?.adminKey
|
||||
if (adminKey == null || adminKey.isEmpty()) {
|
||||
return@withContext // We don't have the admin key, we can't process the invite response
|
||||
}
|
||||
|
||||
configFactory.withGroupConfigsOrNull(groupId) { info, members, keys ->
|
||||
val member = members.get(sender.hexString)
|
||||
if (member == null) {
|
||||
configFactory.withMutableGroupConfigs(groupId) { configs ->
|
||||
val member = configs.groupMembers.get(sender.hexString)
|
||||
if (member != null) {
|
||||
configs.groupMembers.set(member.setAccepted())
|
||||
} else {
|
||||
Log.e(TAG, "User wasn't in the group membership to add!")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
members.set(member.setAccepted())
|
||||
|
||||
configFactory.saveGroupConfigs(keys, info, members)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
|
||||
Destination.ClosedGroup(groupId.hexString)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,26 +800,24 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
pollerFactory.pollerFor(groupId)?.stop()
|
||||
|
||||
val userId = requireNotNull(storage.getUserPublicKey()) { "No current user available" }
|
||||
val userGroups =
|
||||
requireNotNull(configFactory.userGroups) { "User groups config is not available" }
|
||||
val group = userGroups.getClosedGroup(groupId.hexString) ?: return@withContext
|
||||
val group = configFactory.getClosedGroup(groupId) ?: return@withContext
|
||||
|
||||
// Retrieve the group name one last time from the group info,
|
||||
// as we are going to clear the keys, we won't have the chance to
|
||||
// read the group name anymore.
|
||||
val groupName = configFactory.getGroupInfoConfig(groupId)
|
||||
?.use { it.getName() }
|
||||
?: group.name
|
||||
val groupName = configFactory.withGroupConfigs(groupId) { configs ->
|
||||
configs.groupInfo.getName()
|
||||
}
|
||||
|
||||
userGroups.set(
|
||||
group.copy(
|
||||
authData = null,
|
||||
adminKey = null,
|
||||
name = groupName
|
||||
configFactory.withMutableUserConfigs {
|
||||
it.userGroups.set(
|
||||
group.copy(
|
||||
authData = null,
|
||||
adminKey = null,
|
||||
name = groupName
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
configFactory.persist(userGroups, SnodeAPI.nowWithOffset)
|
||||
}
|
||||
|
||||
storage.insertIncomingInfoMessage(
|
||||
context = MessagingModuleConfiguration.shared.context,
|
||||
@@ -898,17 +835,10 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
withContext(dispatcher) {
|
||||
val adminKey = requireAdminAccess(groupId)
|
||||
|
||||
configFactory.getGroupInfoConfig(groupId)?.use { infoConfig ->
|
||||
infoConfig.setName(newName)
|
||||
configFactory.persist(
|
||||
infoConfig,
|
||||
SnodeAPI.nowWithOffset,
|
||||
forPublicKey = groupId.hexString
|
||||
)
|
||||
configFactory.withMutableGroupConfigs(groupId) {
|
||||
it.groupInfo.setName(newName)
|
||||
}
|
||||
|
||||
val groupDestination = Destination.ClosedGroup(groupId.hexString)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination)
|
||||
val timestamp = SnodeAPI.nowWithOffset
|
||||
val signature = SodiumUtilities.sign(
|
||||
buildInfoChangeVerifier(GroupUpdateInfoChangeMessage.Type.NAME, timestamp),
|
||||
@@ -944,9 +874,7 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
// meanwhile, if we are admin we can just delete those messages from the group swarm, and otherwise
|
||||
// the admins can pick up the group message and delete the messages on our behalf.
|
||||
|
||||
val userGroups =
|
||||
requireNotNull(configFactory.userGroups) { "User groups config is not available" }
|
||||
val group = requireNotNull(userGroups.getClosedGroup(groupId.hexString)) {
|
||||
val group = requireNotNull(configFactory.getClosedGroup(groupId)) {
|
||||
"Group doesn't exist"
|
||||
}
|
||||
val userPubKey = requireNotNull(storage.getUserPublicKey()) { "No current user available" }
|
||||
@@ -965,11 +893,8 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
|
||||
// If we are admin, we can delete the messages from the group swarm
|
||||
group.adminKey?.let { adminKey ->
|
||||
deleteMessageFromGroupSwarm(
|
||||
groupId,
|
||||
OwnedSwarmAuth.ofClosedGroup(groupId, adminKey),
|
||||
messageHashes
|
||||
)
|
||||
SnodeAPI.deleteMessage(groupId.hexString, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), messageHashes)
|
||||
.await()
|
||||
}
|
||||
|
||||
// Construct a message to ask members to delete the messages, sign if we are admin, then send
|
||||
@@ -1043,7 +968,7 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val adminKey = configFactory.userGroups?.getClosedGroup(groupId.hexString)?.adminKey
|
||||
val adminKey = configFactory.getClosedGroup(groupId)?.adminKey
|
||||
if (!senderIsVerifiedAdmin && adminKey != null) {
|
||||
// If the deletion request comes from a non-admin, and we as an admin, will also delete
|
||||
// the content from the swarm, provided that the messages are actually sent by that user
|
||||
@@ -1053,11 +978,8 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
groupId.hexString
|
||||
)
|
||||
) {
|
||||
deleteMessageFromGroupSwarm(
|
||||
groupId,
|
||||
OwnedSwarmAuth.ofClosedGroup(groupId, adminKey),
|
||||
hashes
|
||||
)
|
||||
SnodeAPI.deleteMessage(groupId.hexString, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), hashes)
|
||||
.await()
|
||||
}
|
||||
|
||||
// The non-admin user shouldn't be able to delete other user's messages so we will
|
||||
@@ -1065,16 +987,6 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteMessageFromGroupSwarm(
|
||||
groupId: AccountId,
|
||||
auth: OwnedSwarmAuth,
|
||||
hashes: List<String>
|
||||
) {
|
||||
SnodeAPI.sendBatchRequest(
|
||||
groupId, SnodeAPI.buildAuthenticatedDeleteBatchInfo(auth, hashes)
|
||||
)
|
||||
}
|
||||
|
||||
private fun BatchResponse.requireAllRequestsSuccessful(errorMessage: String) {
|
||||
val firstError = this.results.firstOrNull { it.code != 200 }
|
||||
require(firstError == null) { "$errorMessage: ${firstError!!.body}" }
|
||||
|
@@ -9,15 +9,13 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.scan
|
||||
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 org.session.libsession.database.userAuth
|
||||
import org.session.libsession.messaging.notifications.TokenFetcher
|
||||
import org.session.libsession.snode.OwnedSwarmAuth
|
||||
@@ -59,7 +57,7 @@ constructor(
|
||||
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
combine(
|
||||
configFactory.configUpdateNotifications
|
||||
(configFactory.configUpdateNotifications as Flow<Any>)
|
||||
.debounce(500L)
|
||||
.onStart { emit(Unit) },
|
||||
IdentityKeyUtil.CHANGES.onStart { emit(Unit) },
|
||||
@@ -73,13 +71,9 @@ constructor(
|
||||
val userAuth =
|
||||
storage.userAuth ?: return@combine emptyMap<SubscriptionKey, Subscription>()
|
||||
getGroupSubscriptions(
|
||||
token = token,
|
||||
userSecretKey = userAuth.ed25519PrivateKey
|
||||
token = token
|
||||
) + mapOf(
|
||||
SubscriptionKey(userAuth.accountId, token) to OwnedSubscription(
|
||||
userAuth,
|
||||
0
|
||||
)
|
||||
SubscriptionKey(userAuth.accountId, token) to Subscription(userAuth, 0)
|
||||
)
|
||||
}
|
||||
.scan<Map<SubscriptionKey, Subscription>, Pair<Map<SubscriptionKey, Subscription>, Map<SubscriptionKey, Subscription>>?>(
|
||||
@@ -106,13 +100,11 @@ constructor(
|
||||
val subscription = current.getValue(key)
|
||||
async {
|
||||
try {
|
||||
subscription.withAuth { auth ->
|
||||
pushRegistry.register(
|
||||
token = key.token,
|
||||
swarmAuth = auth,
|
||||
namespaces = listOf(subscription.namespace)
|
||||
)
|
||||
}
|
||||
pushRegistry.register(
|
||||
token = key.token,
|
||||
swarmAuth = subscription.auth,
|
||||
namespaces = listOf(subscription.namespace)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to register for push notification", e)
|
||||
}
|
||||
@@ -123,12 +115,10 @@ constructor(
|
||||
val subscription = prev.getValue(key)
|
||||
async {
|
||||
try {
|
||||
subscription.withAuth { auth ->
|
||||
pushRegistry.unregister(
|
||||
token = key.token,
|
||||
swarmAuth = auth,
|
||||
)
|
||||
}
|
||||
pushRegistry.unregister(
|
||||
token = key.token,
|
||||
swarmAuth = subscription.auth,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to unregister for push notification", e)
|
||||
}
|
||||
@@ -141,17 +131,16 @@ constructor(
|
||||
}
|
||||
|
||||
private fun getGroupSubscriptions(
|
||||
token: String,
|
||||
userSecretKey: ByteArray
|
||||
token: String
|
||||
): Map<SubscriptionKey, Subscription> {
|
||||
return buildMap {
|
||||
val groups = configFactory.userGroups?.allClosedGroupInfo().orEmpty()
|
||||
val groups = configFactory.withUserConfigs { it.userGroups.allClosedGroupInfo() }
|
||||
for (group in groups) {
|
||||
val adminKey = group.adminKey
|
||||
if (adminKey != null && adminKey.isNotEmpty()) {
|
||||
put(
|
||||
SubscriptionKey(group.groupAccountId, token),
|
||||
OwnedSubscription(
|
||||
Subscription(
|
||||
auth = OwnedSwarmAuth.ofClosedGroup(group.groupAccountId, adminKey),
|
||||
namespace = Namespace.GROUPS()
|
||||
)
|
||||
@@ -161,15 +150,11 @@ constructor(
|
||||
|
||||
val authData = group.authData
|
||||
if (authData != null && authData.isNotEmpty()) {
|
||||
val subscription =
|
||||
configFactory.withGroupConfigsOrNull(group.groupAccountId) { info, members, keys ->
|
||||
SubAccountSubscription(
|
||||
authData = authData,
|
||||
groupInfoConfigDump = info.dump(),
|
||||
groupMembersConfigDump = members.dump(),
|
||||
groupKeysConfigDump = keys.dump(),
|
||||
groupId = group.groupAccountId,
|
||||
userSecretKey = userSecretKey
|
||||
val subscription = configFactory.getGroupAuth(group.groupAccountId)
|
||||
?.let {
|
||||
Subscription(
|
||||
auth = it,
|
||||
namespace = Namespace.GROUPS()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -181,53 +166,6 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private data class SubscriptionKey(
|
||||
val accountId: AccountId,
|
||||
val token: String,
|
||||
)
|
||||
|
||||
private sealed interface Subscription {
|
||||
suspend fun withAuth(cb: suspend (SwarmAuth) -> Unit)
|
||||
val namespace: Int
|
||||
}
|
||||
|
||||
private class OwnedSubscription(val auth: OwnedSwarmAuth, override val namespace: Int) :
|
||||
Subscription {
|
||||
override suspend fun withAuth(cb: suspend (SwarmAuth) -> Unit) {
|
||||
cb(auth)
|
||||
}
|
||||
}
|
||||
|
||||
private class SubAccountSubscription(
|
||||
val groupId: AccountId,
|
||||
val userSecretKey: ByteArray,
|
||||
val authData: ByteArray,
|
||||
val groupInfoConfigDump: ByteArray,
|
||||
val groupMembersConfigDump: ByteArray,
|
||||
val groupKeysConfigDump: ByteArray
|
||||
) : Subscription {
|
||||
override suspend fun withAuth(cb: suspend (SwarmAuth) -> Unit) {
|
||||
GroupInfoConfig.newInstance(groupId.pubKeyBytes, initialDump = groupInfoConfigDump)
|
||||
.use { info ->
|
||||
GroupMembersConfig.newInstance(
|
||||
groupId.pubKeyBytes,
|
||||
initialDump = groupMembersConfigDump
|
||||
).use { members ->
|
||||
GroupKeysConfig.newInstance(
|
||||
userSecretKey = userSecretKey,
|
||||
groupPublicKey = groupId.pubKeyBytes,
|
||||
initialDump = groupKeysConfigDump,
|
||||
info = info,
|
||||
members = members
|
||||
).use { keys ->
|
||||
cb(GroupSubAccountSwarmAuth(keys, groupId, authData))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val namespace: Int
|
||||
get() = Namespace.GROUPS()
|
||||
}
|
||||
|
||||
private data class SubscriptionKey(val accountId: AccountId, val token: String)
|
||||
private data class Subscription(val auth: SwarmAuth, val namespace: Int)
|
||||
}
|
@@ -28,8 +28,6 @@ class LoadingActivity: BaseActionBarActivity() {
|
||||
private val viewModel: LoadingViewModel by viewModels()
|
||||
|
||||
private fun register(loadFailed: Boolean) {
|
||||
prefs.setLastConfigurationSyncTime(System.currentTimeMillis())
|
||||
|
||||
when {
|
||||
loadFailed -> startPickDisplayNameActivity(loadFailed = true)
|
||||
else -> startHomeActivity(isNewAccount = false, isFromOnboarding = true)
|
||||
|
@@ -16,12 +16,16 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.timeout
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsession.utilities.ConfigUpdateNotification
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
@@ -43,7 +47,8 @@ private val REFRESH_TIME = 50.milliseconds
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
@HiltViewModel
|
||||
internal class LoadingViewModel @Inject constructor(
|
||||
val prefs: TextSecurePreferences
|
||||
val prefs: TextSecurePreferences,
|
||||
val configFactory: ConfigFactoryProtocol,
|
||||
): ViewModel() {
|
||||
|
||||
private val state = MutableStateFlow(State.LOADING)
|
||||
@@ -65,14 +70,19 @@ internal class LoadingViewModel @Inject constructor(
|
||||
.collectLatest { _progress.value = it }
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
TextSecurePreferences.events
|
||||
.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }
|
||||
.onStart { emit(TextSecurePreferences.CONFIGURATION_SYNCED) }
|
||||
.filter { prefs.getConfigurationMessageSynced() }
|
||||
.timeout(TIMEOUT_TIME)
|
||||
.collectLatest { onSuccess() }
|
||||
configFactory.configUpdateNotifications
|
||||
.filter { it == ConfigUpdateNotification.UserConfigs }
|
||||
.onStart { emit(ConfigUpdateNotification.UserConfigs) }
|
||||
.filter {
|
||||
configFactory.withUserConfigs { configs ->
|
||||
!configs.userProfile.getName().isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
// .timeout(TIMEOUT_TIME)
|
||||
.first()
|
||||
onSuccess()
|
||||
} catch (e: Exception) {
|
||||
onFail()
|
||||
}
|
||||
@@ -80,19 +90,15 @@ internal class LoadingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun onSuccess() {
|
||||
withContext(Dispatchers.Main) {
|
||||
state.value = State.SUCCESS
|
||||
delay(IDLE_DONE_TIME)
|
||||
_events.emit(Event.SUCCESS)
|
||||
}
|
||||
}
|
||||
state.value = State.SUCCESS
|
||||
delay(IDLE_DONE_TIME)
|
||||
_events.emit(Event.SUCCESS)
|
||||
}
|
||||
|
||||
private suspend fun onFail() {
|
||||
withContext(Dispatchers.Main) {
|
||||
state.value = State.FAIL
|
||||
delay(IDLE_DONE_TIME)
|
||||
_events.emit(Event.TIMEOUT)
|
||||
}
|
||||
state.value = State.FAIL
|
||||
delay(IDLE_DONE_TIME)
|
||||
_events.emit(Event.TIMEOUT)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,8 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.session.libsession.utilities.AppTextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
|
||||
// Globally accessible composition local objects
|
||||
val LocalColors = compositionLocalOf <ThemeColors> { ClassicDark() }
|
||||
@@ -32,11 +34,10 @@ fun invalidateComposeThemeColors() {
|
||||
*/
|
||||
@Composable
|
||||
fun SessionMaterialTheme(
|
||||
preferences: TextSecurePreferences =
|
||||
(LocalContext.current.applicationContext as ApplicationContext).textSecurePreferences,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val preferences = AppTextSecurePreferences(context)
|
||||
|
||||
val cachedColors = cachedColorsProvider ?: preferences.getColorsProvider().also { cachedColorsProvider = it }
|
||||
|
||||
SessionMaterialTheme(
|
||||
|
Reference in New Issue
Block a user