Config revamp

This commit is contained in:
SessionHero01 2024-10-01 17:20:50 +10:00
parent e9e67068cd
commit 45a66d0eea
No known key found for this signature in database
29 changed files with 1083 additions and 963 deletions

View File

@ -39,18 +39,17 @@ import androidx.lifecycle.ProcessLifecycleOwner;
import com.squareup.phrase.Phrase; import com.squareup.phrase.Phrase;
import org.conscrypt.Conscrypt; 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.database.MessageDataProvider;
import org.session.libsession.messaging.MessagingModuleConfiguration; 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.GroupManagerV2;
import org.session.libsession.messaging.groups.RemoveGroupMemberHandler;
import org.session.libsession.messaging.notifications.TokenFetcher; import org.session.libsession.messaging.notifications.TokenFetcher;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; 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.LegacyClosedGroupPollerV2;
import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.snode.SnodeModule; import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
import org.session.libsession.utilities.Device; import org.session.libsession.utilities.Device;
import org.session.libsession.utilities.Environment; import org.session.libsession.utilities.Environment;
import org.session.libsession.utilities.ProfilePictureUtilities; 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.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager; import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository; import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster; import org.thoughtcrime.securesms.util.Broadcaster;
@ -104,12 +102,10 @@ import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory;
import org.webrtc.PeerConnectionFactory.InitializationOptions; import org.webrtc.PeerConnectionFactory.InitializationOptions;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.Security; import java.security.Security;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Timer; import java.util.Timer;
@ -119,11 +115,8 @@ import javax.inject.Inject;
import dagger.hilt.EntryPoints; import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp; import dagger.hilt.android.HiltAndroidApp;
import kotlin.Unit;
import network.loki.messenger.BuildConfig; import network.loki.messenger.BuildConfig;
import network.loki.messenger.R; 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. * 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; @Inject SSKEnvironment.ProfileManagerProtocol profileManager;
CallMessageProcessor callMessageProcessor; CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration; MessagingModuleConfiguration messagingModuleConfiguration;
@Inject ConfigSyncHandler configSyncHandler;
@Inject RemoveGroupMemberHandler removeGroupMemberHandler;
private volatile boolean isAppVisible; private volatile boolean isAppVisible;
@ -272,6 +267,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet); HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
pushRegistrationHandler.run(); pushRegistrationHandler.run();
configSyncHandler.start();
removeGroupMemberHandler.start();
// add our shortcut debug menu if we are not in a release build // add our shortcut debug menu if we are not in a release build
if (BuildConfig.BUILD_TYPE != "release") { if (BuildConfig.BUILD_TYPE != "release") {
@ -355,6 +352,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return typingStatusSender; return typingStatusSender;
} }
public TextSecurePreferences getTextSecurePreferences() {
return textSecurePreferences;
}
public ReadReceiptManager getReadReceiptManager() { public ReadReceiptManager getReadReceiptManager() {
return readReceiptManager; return readReceiptManager;
} }
@ -444,13 +445,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private static class ProviderInitializationException extends RuntimeException { } private static class ProviderInitializationException extends RuntimeException { }
private void setUpPollingIfNeeded() { private void setUpPollingIfNeeded() {
String userPublicKey = TextSecurePreferences.getLocalNumber(this); String userPublicKey = textSecurePreferences.getLocalNumber();
if (userPublicKey == null) return; if (userPublicKey == null) return;
if (poller != null) { poller = new Poller(configFactory, storage, lokiAPIDatabase);
poller.setUserPublicKey(userPublicKey);
return;
}
poller = new Poller(configFactory);
} }
public void startPollingIfNeeded() { public void startPollingIfNeeded() {

View File

@ -11,9 +11,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.utilities.AppTextSecurePreferences import org.session.libsession.utilities.AppTextSecurePreferences
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.Toaster 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.groups.GroupManagerV2Impl
import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.repository.DefaultConversationRepository import org.thoughtcrime.securesms.repository.DefaultConversationRepository
@ -35,6 +38,12 @@ abstract class AppModule {
@Binds @Binds
abstract fun bindProfileManager(profileManager: ProfileManager): SSKEnvironment.ProfileManagerProtocol abstract fun bindProfileManager(profileManager: ProfileManager): SSKEnvironment.ProfileManagerProtocol
@Binds
abstract fun bindConfigFactory(configFactory: ConfigFactory): ConfigFactoryProtocol
@Binds
abstract fun bindLokiAPIDatabaseProtocol(lokiAPIDatabase: LokiAPIDatabase): LokiAPIDatabaseProtocol
} }
@Module @Module

View File

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.dependencies package org.thoughtcrime.securesms.dependencies
import android.content.Context import android.content.Context
import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
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.ConfigBase 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.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.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.ConfigPush
import network.loki.messenger.libsession_util.util.Contact import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupInfo 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.snode.SwarmAuth
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ConfigFactoryProtocol 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.ConfigUpdateNotification
import org.session.libsession.utilities.GroupConfigs import org.session.libsession.utilities.GroupConfigs
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MutableGroupConfigs import org.session.libsession.utilities.MutableGroupConfigs
import org.session.libsession.utilities.MutableUserConfigs 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.libsession.utilities.UserConfigs
import org.session.libsignal.crypto.ecc.DjbECPublicKey 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.Hex
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.ConfigDatabase import org.thoughtcrime.securesms.database.ConfigDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.GroupManager
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton
class ConfigFactory( @Singleton
private val context: Context, class ConfigFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val configDatabase: ConfigDatabase, private val configDatabase: ConfigDatabase,
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
private val storage: StorageProtocol, private val lokiThreadDatabase: LokiThreadDatabase,
private val storage: Lazy<StorageProtocol>,
private val textSecurePreferences: TextSecurePreferences
) : 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
@ -182,7 +195,7 @@ class ConfigFactory(
members = groupMembers members = groupMembers
) )
fun persistIfDirty(): Boolean { fun dumpIfNeeded(): Boolean {
if (groupInfo.needsDump() || groupMembers.needsDump() || groupKeys.needsDump()) { if (groupInfo.needsDump() || groupMembers.needsDump() || groupKeys.needsDump()) {
configDatabase.storeGroupConfigs( configDatabase.storeGroupConfigs(
publicKey = groupAccountId.hexString, publicKey = groupAccountId.hexString,
@ -197,11 +210,10 @@ class ConfigFactory(
return false return false
} }
override fun loadKeys(message: ByteArray, hash: String, timestamp: Long): Boolean { val isDirty: Boolean
return groupKeys.loadKey(message, hash, timestamp, groupInfo.pointer, groupMembers.pointer) get() = groupInfo.dirty() || groupMembers.dirty()
}
override fun rekeys() { override fun rekey() {
groupKeys.rekey(groupInfo.pointer, groupMembers.pointer) groupKeys.rekey(groupInfo.pointer, groupMembers.pointer)
} }
} }
@ -211,17 +223,17 @@ class ConfigFactory(
private val _configUpdateNotifications = MutableSharedFlow<ConfigUpdateNotification>( private val _configUpdateNotifications = MutableSharedFlow<ConfigUpdateNotification>(
extraBufferCapacity = 1, extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.SUSPEND
) )
override val configUpdateNotifications get() = _configUpdateNotifications override val configUpdateNotifications get() = _configUpdateNotifications
private fun requiresCurrentUserAccountId(): AccountId = private fun requiresCurrentUserAccountId(): AccountId =
AccountId(requireNotNull(storage.getUserPublicKey()) { AccountId(requireNotNull(textSecurePreferences.getLocalNumber()) {
"No logged in user" "No logged in user"
}) })
private fun requiresCurrentUserED25519SecKey(): ByteArray = private fun requiresCurrentUserED25519SecKey(): ByteArray =
requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) { requireNotNull(storage.get().getUserED25519KeyPair()?.secretKey?.asBytes) {
"No logged in user" "No logged in user"
} }
@ -233,7 +245,7 @@ class ConfigFactory(
userAccountId, userAccountId,
threadDb = threadDb, threadDb = threadDb,
configDatabase = configDatabase, 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 -> 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) _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 { override fun <T> withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T {
val configs = groupConfigs.getOrPut(groupId) { val configs = groupConfigs.getOrPut(groupId) {
val groupAdminKey = requireNotNull(withUserConfigs { 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( override fun <T> withMutableGroupConfigs(
groupId: AccountId, groupId: AccountId,
cb: (MutableGroupConfigs) -> T cb: (MutableGroupConfigs) -> T
): T { ): T {
return withGroupConfigs(groupId) { configs -> return doWithMutableGroupConfigs(groupId) {
val result = cb(configs as GroupConfigsImpl) cb(it) to it.dumpIfNeeded()
if (configs.persistIfDirty()) {
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsUpdated(groupId))
}
result
} }
} }
@ -309,7 +362,7 @@ class ConfigFactory(
): ByteArray? { ): ByteArray? {
return Sodium.decryptForMultipleSimple( return Sodium.decryptForMultipleSimple(
encoded = encoded, encoded = encoded,
ed25519SecretKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) { ed25519SecretKey = requireNotNull(storage.get().getUserED25519KeyPair()?.secretKey?.asBytes) {
"No logged in user" "No logged in user"
}, },
domain = domain, 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( override fun conversationInConfig(
publicKey: String?, publicKey: String?,
groupPublicKey: String?, groupPublicKey: String?,
openGroupId: String?, openGroupId: String?,
visibleOnly: Boolean visibleOnly: Boolean
): Boolean { ): Boolean {
val userPublicKey = storage.getUserPublicKey() ?: return false val userPublicKey = storage.get().getUserPublicKey() ?: return false
if (openGroupId != null) { if (openGroupId != null) {
val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context) val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context)
val openGroup = val openGroup = 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 withUserConfigs { return withUserConfigs {

View File

@ -7,6 +7,7 @@ import network.loki.messenger.libsession_util.util.GroupInfo
import org.session.libsession.database.StorageProtocol 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.database.LokiAPIDatabaseProtocol
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -15,7 +16,8 @@ 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 storage: Lazy<StorageProtocol>,
private val lokiApiDatabase: LokiAPIDatabaseProtocol,
) { ) {
private val pollers = ConcurrentHashMap<AccountId, ClosedGroupPoller>() private val pollers = ConcurrentHashMap<AccountId, ClosedGroupPoller>()
@ -29,7 +31,15 @@ class PollerFactory(
if (invited != false) return null if (invited != false) return null
return pollers.getOrPut(sessionId) { 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,
)
} }
} }

View File

@ -14,6 +14,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import org.session.libsession.database.StorageProtocol 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.ConfigFactoryProtocol
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.thoughtcrime.securesms.database.ConfigDatabase import org.thoughtcrime.securesms.database.ConfigDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import javax.inject.Named import javax.inject.Named
@ -26,15 +28,6 @@ object SessionUtilModule {
private const val POLLER_SCOPE = "poller_coroutine_scope" 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 @Provides
@Named(POLLER_SCOPE) @Named(POLLER_SCOPE)
fun providePollerScope(): CoroutineScope = GlobalScope fun providePollerScope(): CoroutineScope = GlobalScope
@ -49,12 +42,14 @@ 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,
storage: StorageProtocol, storage: Lazy<StorageProtocol>,
groupManagerV2: Lazy<GroupManagerV2>) = PollerFactory( groupManagerV2: Lazy<GroupManagerV2>,
lokiApiDatabase: LokiAPIDatabaseProtocol) = PollerFactory(
scope = coroutineScope, scope = coroutineScope,
executor = dispatcher, executor = dispatcher,
configFactory = configFactory, configFactory = configFactory,
groupManagerV2 = groupManagerV2, groupManagerV2 = groupManagerV2,
storage = storage storage = storage,
lokiApiDatabase = lokiApiDatabase,
) )
} }

View File

@ -7,7 +7,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import dagger.hilt.android.AndroidEntryPoint
import org.thoughtcrime.securesms.conversation.start.NullStartConversationDelegate import org.thoughtcrime.securesms.conversation.start.NullStartConversationDelegate
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2

View File

@ -5,9 +5,11 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.groups.compose.EditGroupScreen import org.thoughtcrime.securesms.groups.compose.EditGroupScreen
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class EditGroupActivity: PassphraseRequiredActionBarActivity() { class EditGroupActivity: PassphraseRequiredActionBarActivity() {

View File

@ -8,6 +8,7 @@ import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow 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. // Output: the source-of-truth group information. Other states are derived from this.
private val groupInfo: StateFlow<Pair<GroupDisplayInfo, List<GroupMemberState>>?> = private val groupInfo: StateFlow<Pair<GroupDisplayInfo, List<GroupMemberState>>?> =
configFactory.configUpdateNotifications (configFactory.configUpdateNotifications as Flow<Any>)
.onStart { emit(Unit) } .onStart { emit(Unit) }
.map { .map {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {

View File

@ -3,19 +3,14 @@ package org.thoughtcrime.securesms.groups
import android.content.Context import android.content.Context
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
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.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.Conversation
import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.GroupInfo
import network.loki.messenger.libsession_util.util.GroupMember 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.INVITE_STATUS_SENT
import network.loki.messenger.libsession_util.util.Sodium import network.loki.messenger.libsession_util.util.Sodium
import network.loki.messenger.libsession_util.util.UserPic 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.MessagingModuleConfiguration
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.ConfigurationSyncJob.Companion.messageInformation
import org.session.libsession.messaging.jobs.InviteContactsJob import org.session.libsession.messaging.jobs.InviteContactsJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.messages.Destination 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.SnodeAPI
import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.model.BatchResponse 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.snode.utilities.await
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.getClosedGroup
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.withGroupConfigsOrNull
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
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage 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.database.MmsSmsDatabase
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.PollerFactory import org.thoughtcrime.securesms.dependencies.PollerFactory
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -97,8 +89,6 @@ class GroupManagerV2Impl @Inject constructor(
): Recipient = withContext(dispatcher) { ): Recipient = withContext(dispatcher) {
val ourAccountId = val ourAccountId =
requireNotNull(storage.getUserPublicKey()) { "Our account ID is not available" } 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 ourProfile = storage.getUserProfile()
val groupCreationTimestamp = SnodeAPI.nowWithOffset val groupCreationTimestamp = SnodeAPI.nowWithOffset
@ -108,9 +98,8 @@ class GroupManagerV2Impl @Inject constructor(
configs.userGroups.createGroup().also(configs.userGroups::set) 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 groupId = group.groupAccountId
val groupAuth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey)
try { try {
configFactory.withMutableGroupConfigs(groupId) { configs -> configFactory.withMutableGroupConfigs(groupId) { configs ->
@ -141,7 +130,7 @@ class GroupManagerV2Impl @Inject constructor(
) )
// Manually re-key to prevent issue with linked admin devices // Manually re-key to prevent issue with linked admin devices
configs.rekeys() configs.rekey()
} }
configFactory.withMutableUserConfigs { configFactory.withMutableUserConfigs {
@ -238,10 +227,10 @@ class GroupManagerV2Impl @Inject constructor(
) )
) )
} else { } 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) { override suspend fun handleMemberLeft(message: GroupUpdated, closedGroupId: AccountId) {
val userGroups = configFactory.userGroups ?: return
val closedGroupHexString = closedGroupId.hexString val closedGroupHexString = closedGroupId.hexString
val closedGroup = userGroups.getClosedGroup(closedGroupId.hexString) ?: return val closedGroup =
configFactory.withUserConfigs { it.userGroups.getClosedGroup(closedGroupId.hexString) }
?: return
if (closedGroup.hasAdminKey()) { if (closedGroup.hasAdminKey()) {
// re-key and do a new config removing the previous member // re-key and do a new config removing the previous member
doRemoveMembers( doRemoveMembers(
@ -313,22 +303,27 @@ class GroupManagerV2Impl @Inject constructor(
removeMemberMessages = false removeMemberMessages = false
) )
} else { } else {
configFactory.getGroupMemberConfig(closedGroupId)?.use { memberConfig -> val hasAnyAdminRemaining = configFactory.withGroupConfigs(closedGroupId) { configs ->
// if the leaving member is an admin, disable the group and remove it configs.groupMembers.all()
// This is just to emulate the "existing" group behaviour, this will need to be removed in future .asSequence()
if (memberConfig.get(message.sender!!)?.admin == true) { .filterNot { it.sessionId == message.sender }
pollerFactory.pollerFor(closedGroupId)?.stop() .any { it.admin && !it.removed }
storage.getThreadId(Address.fromSerialized(closedGroupHexString)) }
?.let(storage::deleteConversation)
configFactory.removeGroup(closedGroupId) // 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) { override suspend fun leaveGroup(group: AccountId, deleteOnLeave: Boolean) {
val canSendGroupMessage = val canSendGroupMessage =
configFactory.userGroups?.getClosedGroup(group.hexString)?.kicked != true configFactory.withUserConfigs { it.userGroups.getClosedGroup(group.hexString) }?.kicked != true
val address = Address.fromSerialized(group.hexString) val address = Address.fromSerialized(group.hexString)
if (canSendGroupMessage) { if (canSendGroupMessage) {
@ -358,79 +353,81 @@ class GroupManagerV2Impl @Inject constructor(
if (deleteOnLeave) { if (deleteOnLeave) {
storage.getThreadId(address)?.let(storage::deleteConversation) storage.getThreadId(address)?.let(storage::deleteConversation)
configFactory.removeGroup(group) configFactory.removeGroup(group)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application)
} }
} }
override suspend fun promoteMember(group: AccountId, members: List<AccountId>): Unit = override suspend fun promoteMember(
withContext(dispatcher) { group: AccountId,
val adminKey = requireAdminAccess(group) members: List<AccountId>
): Unit = withContext(dispatcher) {
val adminKey = requireAdminAccess(group)
val groupName = configFactory.withGroupConfigs(group) { it.groupInfo.getName() }
configFactory.withGroupConfigsOrNull(group) { info, membersConfig, keys -> // Send out the promote message to the members concurrently
// Promote the members by sending a message containing the admin key to each member's swarm, val promotionDeferred = members.associateWith { member ->
// we do this concurrently and then update the group configs after all the messages are sent. async {
val promoteResult = members.asSequence() val message = GroupUpdated(
.mapNotNull { membersConfig.get(it.hexString) } GroupUpdateMessage.newBuilder()
.map { memberConfig -> .setPromoteMessage(
async { DataMessage.GroupUpdatePromoteMessage.newBuilder()
val message = GroupUpdated( .setGroupIdentitySeed(ByteString.copyFrom(adminKey))
GroupUpdateMessage.newBuilder() .setName(groupName)
.setPromoteMessage( )
DataMessage.GroupUpdatePromoteMessage.newBuilder() .build()
.setGroupIdentitySeed(ByteString.copyFrom(adminKey)) )
.setName(info.getName())
)
.build()
)
try { MessageSender.sendNonDurably(
MessageSender.sendNonDurably( message = message,
message = message, address = Address.fromSerialized(member.hexString),
address = Address.fromSerialized(memberConfig.sessionId), isSyncMessage = false
isSyncMessage = false ).await()
).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)
} }
// 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( private suspend fun doRemoveMembers(
group: AccountId, group: AccountId,
removedMembers: List<AccountId>, removedMembers: List<AccountId>,
@ -440,26 +437,27 @@ class GroupManagerV2Impl @Inject constructor(
val adminKey = requireAdminAccess(group) val adminKey = requireAdminAccess(group)
val groupAuth = OwnedSwarmAuth.ofClosedGroup(group, adminKey) val groupAuth = OwnedSwarmAuth.ofClosedGroup(group, adminKey)
configFactory.withGroupConfigsOrNull(group) { info, members, keys -> // To remove a member from a group, we need to first:
// To remove a member from a group, we need to first: // 1. Notify the swarm that this member's key has bene revoked
// 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
// 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
// 3. Optionally, send "delete member messages" to the group. (So that every device in the group // delete this member's messages locally.)
// 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.
// 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:
// After these steps are all done, we will do the following: // Update the group configs to remove the member, sync if needed, then
// Update the group configs to remove the member, sync if needed, then // delete the member's messages locally and remotely.
// delete the member's messages locally and remotely.
val essentialRequests = configFactory.withGroupConfigs(group) { configs ->
val messageSendTimestamp = SnodeAPI.nowWithOffset val messageSendTimestamp = SnodeAPI.nowWithOffset
val essentialRequests = buildList { buildList {
this += SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest( this += SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest(
groupAdminAuth = groupAuth, groupAdminAuth = groupAuth,
subAccountTokens = removedMembers.map(keys::getSubAccountToken) subAccountTokens = removedMembers.map(configs.groupKeys::getSubAccountToken)
) )
this += Sodium.encryptForMultipleSimple( this += Sodium.encryptForMultipleSimple(
messages = removedMembers.map { "${it.hexString}-${keys.currentGeneration()}".encodeToByteArray() } messages = removedMembers.map { "${it.hexString}-${configs.groupKeys.currentGeneration()}".encodeToByteArray() }
.toTypedArray(), .toTypedArray(),
recipients = removedMembers.map { it.pubKeyBytes }.toTypedArray(), recipients = removedMembers.map { it.pubKeyBytes }.toTypedArray(),
ed25519SecretKey = adminKey, ed25519SecretKey = adminKey,
@ -506,132 +504,97 @@ class GroupManagerV2Impl @Inject constructor(
) )
} }
} }
}
val snode = SnodeAPI.getSingleTargetSnode(group.hexString).await() val snode = SnodeAPI.getSingleTargetSnode(group.hexString).await()
val responses = SnodeAPI.getBatchResponse( val responses = SnodeAPI.getBatchResponse(
snode, snode,
group.hexString, group.hexString,
essentialRequests, essentialRequests,
sequence = true 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 // Next step: update group configs, rekey, remove member messages if required
val messagesToDelete = mutableListOf<String>() configFactory.withMutableGroupConfigs(group) { configs ->
for (member in removedMembers) { removedMembers.forEach { configs.groupMembers.erase(it.hexString) }
members.erase(member.hexString) configs.rekey()
} }
keys.rekey(info, members) if (removeMemberMessages) {
val threadId = storage.getThreadId(Address.fromSerialized(group.hexString))
if (removeMemberMessages) { if (threadId != null) {
val threadId = storage.getThreadId(Address.fromSerialized(group.hexString)) val messagesToDelete = mutableListOf<String>()
if (threadId != null) { for (member in removedMembers) {
for (member in removedMembers) { for (msg in mmsSmsDatabase.getUserMessages(threadId, member.hexString)) {
for (msg in mmsSmsDatabase.getUserMessages(threadId, member.hexString)) { val serverHash = lokiDatabase.getMessageServerHash(msg.id, msg.isMms)
val serverHash = lokiDatabase.getMessageServerHash(msg.id, msg.isMms) if (serverHash != null) {
if (serverHash != null) { messagesToDelete.add(serverHash)
messagesToDelete.add(serverHash)
}
} }
storage.deleteMessagesByUser(threadId, member.hexString)
} }
}
}
val requests = buildList { storage.deleteMessagesByUser(threadId, member.hexString)
keys.messageInformation(groupAuth)?.let {
this += "Sync keys config messages" to it.batch
} }
this += "Sync info config messages" to info.messageInformation( SnodeAPI.sendBatchRequest(
messagesToDelete, snode, group.hexString, SnodeAPI.buildAuthenticatedDeleteBatchInfo(
groupAuth groupAuth,
).batch messagesToDelete
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))
) )
.build() )
val message = GroupUpdated(
updateMessage
).apply { sentTimestamp = timestamp }
MessageSender.send(message, Destination.ClosedGroup(group.hexString), false)
storage.insertGroupInfoChange(message, group)
} }
} }
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded( if (sendRemovedMessage) {
Destination.ClosedGroup(group.hexString) 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) = override suspend fun respondToInvitation(groupId: AccountId, approved: Boolean) =
withContext(dispatcher) { withContext(dispatcher) {
val groups = requireNotNull(configFactory.userGroups) { val group = requireNotNull(
"User groups config is not available" configFactory.withUserConfigs { it.userGroups.getClosedGroup(groupId.hexString) }
} ) { "User groups config is not available" }
val threadId = val threadId =
checkNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) { checkNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) {
"No thread has been created for the group" "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 // Whether approved or not, delete the invite
lokiDatabase.deleteGroupInviteReferrer(threadId) lokiDatabase.deleteGroupInviteReferrer(threadId)
if (approved) { if (approved) {
approveGroupInvite(groups, group, threadId) approveGroupInvite(group, threadId)
} else { } else {
groups.eraseClosedGroup(groupId.hexString) configFactory.withMutableUserConfigs { it.userGroups.eraseClosedGroup(groupId.hexString) }
storage.deleteConversation(threadId) storage.deleteConversation(threadId)
} }
} }
private fun approveGroupInvite( private fun approveGroupInvite(
groups: UserGroupsConfig,
group: GroupInfo.ClosedGroupInfo, group: GroupInfo.ClosedGroupInfo,
threadId: Long, threadId: Long,
) { ) {
@ -640,9 +603,9 @@ class GroupManagerV2Impl @Inject constructor(
} }
// Clear the invited flag of the group in the config // Clear the invited flag of the group in the config
groups.set(group.copy(invited = false)) configFactory.withMutableUserConfigs { configs ->
configFactory.persist(forConfigObject = groups, timestamp = SnodeAPI.nowWithOffset) configs.userGroups.set(group.copy(invited = false))
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application) }
if (group.adminKey == null) { if (group.adminKey == null) {
// Send an invite response to the group if we are invited as a regular member // Send an invite response to the group if we are invited as a regular member
@ -659,19 +622,13 @@ class GroupManagerV2Impl @Inject constructor(
) )
} else { } else {
// If we are invited as admin, we can just update the group info ourselves // If we are invited as admin, we can just update the group info ourselves
configFactory.withGroupConfigsOrNull(group.groupAccountId) { info, members, keys -> configFactory.withMutableGroupConfigs(group.groupAccountId) { configs ->
members.get(key)?.let { member -> configs.groupMembers.get(key)?.let { member ->
members.set(member.setPromoteSuccess().setAccepted()) configs.groupMembers.set(member.setPromoteSuccess().setAccepted())
configFactory.saveGroupConfigs(keys, info, members)
} }
Unit Unit
} }
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
destination = Destination.ClosedGroup(group.groupAccountId.hexString)
)
} }
pollerFactory.pollerFor(group.groupAccountId)?.start() pollerFactory.pollerFor(group.groupAccountId)?.start()
@ -696,8 +653,12 @@ class GroupManagerV2Impl @Inject constructor(
if (inviteMessageHash != null) { if (inviteMessageHash != null) {
val auth = requireNotNull(storage.userAuth) { "No current user available" } val auth = requireNotNull(storage.userAuth) { "No current user available" }
SnodeAPI.sendBatchRequest( SnodeAPI.sendBatchRequest(
auth.accountId, snode = SnodeAPI.getSingleTargetSnode(groupId.hexString).await(),
SnodeAPI.buildAuthenticatedDeleteBatchInfo(auth, listOf(inviteMessageHash)), publicKey = auth.accountId.hexString,
request = SnodeAPI.buildAuthenticatedDeleteBatchInfo(
auth,
listOf(inviteMessageHash)
),
) )
} }
} }
@ -709,12 +670,9 @@ class GroupManagerV2Impl @Inject constructor(
promoter: AccountId, promoter: AccountId,
promoteMessageHash: String? promoteMessageHash: String?
) = withContext(dispatcher) { ) = withContext(dispatcher) {
val groups = requireNotNull(configFactory.userGroups) {
"User groups config is not available"
}
val userAuth = requireNotNull(storage.userAuth) { "No current user 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 (group == null) {
// If we haven't got the group in the config, it could mean that we haven't // 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 { } else {
// If we have the group in the config, we can just update the admin key // If we have the group in the config, we can just update the admin key
group = group.copy(adminKey = adminKey) configFactory.withMutableUserConfigs {
groups.set(group) it.userGroups.set(group.copy(adminKey = adminKey))
configFactory.persist(groups, SnodeAPI.nowWithOffset) }
// Update our promote state // Update our promote state
configFactory.withGroupConfigsOrNull(groupId) { info, members, keys -> configFactory.withMutableGroupConfigs(groupId) { configs ->
members.get(userAuth.accountId.hexString)?.let { member -> configs.groupMembers.get(userAuth.accountId.hexString)?.let { member ->
members.set(member.setPromoteSuccess()) configs.groupMembers.set(member.setPromoteSuccess())
configFactory.saveGroupConfigs(keys, info, members)
} }
Unit Unit
} }
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
destination = Destination.ClosedGroup(groupId.hexString)
)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application)
} }
// Delete the promotion message remotely // Delete the promotion message remotely
if (promoteMessageHash != null) { if (promoteMessageHash != null) {
SnodeAPI.sendBatchRequest( SnodeAPI.deleteMessage(
userAuth.accountId, userAuth.accountId.hexString,
SnodeAPI.buildAuthenticatedDeleteBatchInfo(userAuth, listOf(promoteMessageHash)), userAuth,
) listOf(promoteMessageHash)
).await()
} }
} }
@ -777,12 +728,8 @@ class GroupManagerV2Impl @Inject constructor(
fromPromotion: Boolean, fromPromotion: Boolean,
inviter: AccountId, 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 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 return
} }
@ -799,15 +746,17 @@ class GroupManagerV2Impl @Inject constructor(
invited = !shouldAutoApprove, invited = !shouldAutoApprove,
name = groupName, name = groupName,
) )
groups.set(closedGroupInfo)
configFactory.persist(groups, SnodeAPI.nowWithOffset) configFactory.withMutableUserConfigs {
it.userGroups.set(closedGroupInfo)
}
profileManager.setName(application, recipient, groupName) profileManager.setName(application, recipient, groupName)
val groupThreadId = storage.getOrCreateThreadIdFor(recipient.address) val groupThreadId = storage.getOrCreateThreadIdFor(recipient.address)
storage.setRecipientApprovedMe(recipient, true) storage.setRecipientApprovedMe(recipient, true)
storage.setRecipientApproved(recipient, shouldAutoApprove) storage.setRecipientApproved(recipient, shouldAutoApprove)
if (shouldAutoApprove) { if (shouldAutoApprove) {
approveGroupInvite(groups, closedGroupInfo, groupThreadId) approveGroupInvite(closedGroupInfo, groupThreadId)
} else { } else {
lokiDatabase.addGroupInviteReferrer(groupThreadId, inviter.hexString) lokiDatabase.addGroupInviteReferrer(groupThreadId, inviter.hexString)
storage.insertGroupInviteControlMessage( storage.insertGroupInviteControlMessage(
@ -829,28 +778,18 @@ class GroupManagerV2Impl @Inject constructor(
return@withContext return@withContext
} }
val groups = requireNotNull(configFactory.userGroups) { val adminKey = configFactory.getClosedGroup(groupId)?.adminKey
"User groups config is not available"
}
val adminKey = groups.getClosedGroup(groupId.hexString)?.adminKey
if (adminKey == null || adminKey.isEmpty()) { if (adminKey == null || adminKey.isEmpty()) {
return@withContext // We don't have the admin key, we can't process the invite response return@withContext // We don't have the admin key, we can't process the invite response
} }
configFactory.withGroupConfigsOrNull(groupId) { info, members, keys -> configFactory.withMutableGroupConfigs(groupId) { configs ->
val member = members.get(sender.hexString) val member = configs.groupMembers.get(sender.hexString)
if (member == null) { if (member != null) {
configs.groupMembers.set(member.setAccepted())
} else {
Log.e(TAG, "User wasn't in the group membership to add!") 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() pollerFactory.pollerFor(groupId)?.stop()
val userId = requireNotNull(storage.getUserPublicKey()) { "No current user available" } val userId = requireNotNull(storage.getUserPublicKey()) { "No current user available" }
val userGroups = val group = configFactory.getClosedGroup(groupId) ?: return@withContext
requireNotNull(configFactory.userGroups) { "User groups config is not available" }
val group = userGroups.getClosedGroup(groupId.hexString) ?: return@withContext
// Retrieve the group name one last time from the group info, // 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 // as we are going to clear the keys, we won't have the chance to
// read the group name anymore. // read the group name anymore.
val groupName = configFactory.getGroupInfoConfig(groupId) val groupName = configFactory.withGroupConfigs(groupId) { configs ->
?.use { it.getName() } configs.groupInfo.getName()
?: group.name }
userGroups.set( configFactory.withMutableUserConfigs {
group.copy( it.userGroups.set(
authData = null, group.copy(
adminKey = null, authData = null,
name = groupName adminKey = null,
name = groupName
)
) )
) }
configFactory.persist(userGroups, SnodeAPI.nowWithOffset)
storage.insertIncomingInfoMessage( storage.insertIncomingInfoMessage(
context = MessagingModuleConfiguration.shared.context, context = MessagingModuleConfiguration.shared.context,
@ -898,17 +835,10 @@ class GroupManagerV2Impl @Inject constructor(
withContext(dispatcher) { withContext(dispatcher) {
val adminKey = requireAdminAccess(groupId) val adminKey = requireAdminAccess(groupId)
configFactory.getGroupInfoConfig(groupId)?.use { infoConfig -> configFactory.withMutableGroupConfigs(groupId) {
infoConfig.setName(newName) it.groupInfo.setName(newName)
configFactory.persist(
infoConfig,
SnodeAPI.nowWithOffset,
forPublicKey = groupId.hexString
)
} }
val groupDestination = Destination.ClosedGroup(groupId.hexString)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination)
val timestamp = SnodeAPI.nowWithOffset val timestamp = SnodeAPI.nowWithOffset
val signature = SodiumUtilities.sign( val signature = SodiumUtilities.sign(
buildInfoChangeVerifier(GroupUpdateInfoChangeMessage.Type.NAME, timestamp), 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 // 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. // the admins can pick up the group message and delete the messages on our behalf.
val userGroups = val group = requireNotNull(configFactory.getClosedGroup(groupId)) {
requireNotNull(configFactory.userGroups) { "User groups config is not available" }
val group = requireNotNull(userGroups.getClosedGroup(groupId.hexString)) {
"Group doesn't exist" "Group doesn't exist"
} }
val userPubKey = requireNotNull(storage.getUserPublicKey()) { "No current user available" } 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 // If we are admin, we can delete the messages from the group swarm
group.adminKey?.let { adminKey -> group.adminKey?.let { adminKey ->
deleteMessageFromGroupSwarm( SnodeAPI.deleteMessage(groupId.hexString, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), messageHashes)
groupId, .await()
OwnedSwarmAuth.ofClosedGroup(groupId, adminKey),
messageHashes
)
} }
// Construct a message to ask members to delete the messages, sign if we are admin, then send // 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 (!senderIsVerifiedAdmin && adminKey != null) {
// If the deletion request comes from a non-admin, and we as an admin, will also delete // 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 // 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 groupId.hexString
) )
) { ) {
deleteMessageFromGroupSwarm( SnodeAPI.deleteMessage(groupId.hexString, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), hashes)
groupId, .await()
OwnedSwarmAuth.ofClosedGroup(groupId, adminKey),
hashes
)
} }
// The non-admin user shouldn't be able to delete other user's messages so we will // 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) { private fun BatchResponse.requireAllRequestsSuccessful(errorMessage: String) {
val firstError = this.results.firstOrNull { it.code != 200 } val firstError = this.results.firstOrNull { it.code != 200 }
require(firstError == null) { "$errorMessage: ${firstError!!.body}" } require(firstError == null) { "$errorMessage: ${firstError!!.body}" }

View File

@ -9,15 +9,13 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.scan
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 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.OwnedSwarmAuth import org.session.libsession.snode.OwnedSwarmAuth
@ -59,7 +57,7 @@ constructor(
job = scope.launch(Dispatchers.Default) { job = scope.launch(Dispatchers.Default) {
combine( combine(
configFactory.configUpdateNotifications (configFactory.configUpdateNotifications as Flow<Any>)
.debounce(500L) .debounce(500L)
.onStart { emit(Unit) }, .onStart { emit(Unit) },
IdentityKeyUtil.CHANGES.onStart { emit(Unit) }, IdentityKeyUtil.CHANGES.onStart { emit(Unit) },
@ -73,13 +71,9 @@ constructor(
val userAuth = val userAuth =
storage.userAuth ?: return@combine emptyMap<SubscriptionKey, Subscription>() storage.userAuth ?: return@combine emptyMap<SubscriptionKey, Subscription>()
getGroupSubscriptions( getGroupSubscriptions(
token = token, token = token
userSecretKey = userAuth.ed25519PrivateKey
) + mapOf( ) + mapOf(
SubscriptionKey(userAuth.accountId, token) to OwnedSubscription( SubscriptionKey(userAuth.accountId, token) to Subscription(userAuth, 0)
userAuth,
0
)
) )
} }
.scan<Map<SubscriptionKey, Subscription>, Pair<Map<SubscriptionKey, Subscription>, Map<SubscriptionKey, Subscription>>?>( .scan<Map<SubscriptionKey, Subscription>, Pair<Map<SubscriptionKey, Subscription>, Map<SubscriptionKey, Subscription>>?>(
@ -106,13 +100,11 @@ constructor(
val subscription = current.getValue(key) val subscription = current.getValue(key)
async { async {
try { try {
subscription.withAuth { auth -> pushRegistry.register(
pushRegistry.register( token = key.token,
token = key.token, swarmAuth = subscription.auth,
swarmAuth = auth, namespaces = listOf(subscription.namespace)
namespaces = listOf(subscription.namespace) )
)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to register for push notification", e) Log.e(TAG, "Failed to register for push notification", e)
} }
@ -123,12 +115,10 @@ constructor(
val subscription = prev.getValue(key) val subscription = prev.getValue(key)
async { async {
try { try {
subscription.withAuth { auth -> pushRegistry.unregister(
pushRegistry.unregister( token = key.token,
token = key.token, swarmAuth = subscription.auth,
swarmAuth = auth, )
)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to unregister for push notification", e) Log.e(TAG, "Failed to unregister for push notification", e)
} }
@ -141,17 +131,16 @@ constructor(
} }
private fun getGroupSubscriptions( private fun getGroupSubscriptions(
token: String, token: String
userSecretKey: ByteArray
): Map<SubscriptionKey, Subscription> { ): Map<SubscriptionKey, Subscription> {
return buildMap { return buildMap {
val groups = configFactory.userGroups?.allClosedGroupInfo().orEmpty() val groups = configFactory.withUserConfigs { it.userGroups.allClosedGroupInfo() }
for (group in groups) { for (group in groups) {
val adminKey = group.adminKey val adminKey = group.adminKey
if (adminKey != null && adminKey.isNotEmpty()) { if (adminKey != null && adminKey.isNotEmpty()) {
put( put(
SubscriptionKey(group.groupAccountId, token), SubscriptionKey(group.groupAccountId, token),
OwnedSubscription( Subscription(
auth = OwnedSwarmAuth.ofClosedGroup(group.groupAccountId, adminKey), auth = OwnedSwarmAuth.ofClosedGroup(group.groupAccountId, adminKey),
namespace = Namespace.GROUPS() namespace = Namespace.GROUPS()
) )
@ -161,15 +150,11 @@ constructor(
val authData = group.authData val authData = group.authData
if (authData != null && authData.isNotEmpty()) { if (authData != null && authData.isNotEmpty()) {
val subscription = val subscription = configFactory.getGroupAuth(group.groupAccountId)
configFactory.withGroupConfigsOrNull(group.groupAccountId) { info, members, keys -> ?.let {
SubAccountSubscription( Subscription(
authData = authData, auth = it,
groupInfoConfigDump = info.dump(), namespace = Namespace.GROUPS()
groupMembersConfigDump = members.dump(),
groupKeysConfigDump = keys.dump(),
groupId = group.groupAccountId,
userSecretKey = userSecretKey
) )
} }
@ -181,53 +166,6 @@ constructor(
} }
} }
private data class SubscriptionKey( private data class SubscriptionKey(val accountId: AccountId, val token: String)
val accountId: AccountId, private data class Subscription(val auth: SwarmAuth, val namespace: Int)
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()
}
} }

View File

@ -28,8 +28,6 @@ class LoadingActivity: BaseActionBarActivity() {
private val viewModel: LoadingViewModel by viewModels() private val viewModel: LoadingViewModel by viewModels()
private fun register(loadFailed: Boolean) { private fun register(loadFailed: Boolean) {
prefs.setLastConfigurationSyncTime(System.currentTimeMillis())
when { when {
loadFailed -> startPickDisplayNameActivity(loadFailed = true) loadFailed -> startPickDisplayNameActivity(loadFailed = true)
else -> startHomeActivity(isNewAccount = false, isFromOnboarding = true) else -> startHomeActivity(isNewAccount = false, isFromOnboarding = true)

View File

@ -16,12 +16,16 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration import kotlin.time.Duration
@ -43,7 +47,8 @@ private val REFRESH_TIME = 50.milliseconds
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@HiltViewModel @HiltViewModel
internal class LoadingViewModel @Inject constructor( internal class LoadingViewModel @Inject constructor(
val prefs: TextSecurePreferences val prefs: TextSecurePreferences,
val configFactory: ConfigFactoryProtocol,
): ViewModel() { ): ViewModel() {
private val state = MutableStateFlow(State.LOADING) private val state = MutableStateFlow(State.LOADING)
@ -65,14 +70,19 @@ internal class LoadingViewModel @Inject constructor(
.collectLatest { _progress.value = it } .collectLatest { _progress.value = it }
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch {
try { try {
TextSecurePreferences.events configFactory.configUpdateNotifications
.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED } .filter { it == ConfigUpdateNotification.UserConfigs }
.onStart { emit(TextSecurePreferences.CONFIGURATION_SYNCED) } .onStart { emit(ConfigUpdateNotification.UserConfigs) }
.filter { prefs.getConfigurationMessageSynced() } .filter {
.timeout(TIMEOUT_TIME) configFactory.withUserConfigs { configs ->
.collectLatest { onSuccess() } !configs.userProfile.getName().isNullOrEmpty()
}
}
// .timeout(TIMEOUT_TIME)
.first()
onSuccess()
} catch (e: Exception) { } catch (e: Exception) {
onFail() onFail()
} }
@ -80,19 +90,15 @@ internal class LoadingViewModel @Inject constructor(
} }
private suspend fun onSuccess() { private suspend fun onSuccess() {
withContext(Dispatchers.Main) { state.value = State.SUCCESS
state.value = State.SUCCESS delay(IDLE_DONE_TIME)
delay(IDLE_DONE_TIME) _events.emit(Event.SUCCESS)
_events.emit(Event.SUCCESS) }
}
}
private suspend fun onFail() { private suspend fun onFail() {
withContext(Dispatchers.Main) { state.value = State.FAIL
state.value = State.FAIL delay(IDLE_DONE_TIME)
delay(IDLE_DONE_TIME) _events.emit(Event.TIMEOUT)
_events.emit(Event.TIMEOUT)
}
} }
} }

View File

@ -15,6 +15,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.session.libsession.utilities.AppTextSecurePreferences import org.session.libsession.utilities.AppTextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext
// Globally accessible composition local objects // Globally accessible composition local objects
val LocalColors = compositionLocalOf <ThemeColors> { ClassicDark() } val LocalColors = compositionLocalOf <ThemeColors> { ClassicDark() }
@ -32,11 +34,10 @@ fun invalidateComposeThemeColors() {
*/ */
@Composable @Composable
fun SessionMaterialTheme( fun SessionMaterialTheme(
preferences: TextSecurePreferences =
(LocalContext.current.applicationContext as ApplicationContext).textSecurePreferences,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val context = LocalContext.current
val preferences = AppTextSecurePreferences(context)
val cachedColors = cachedColorsProvider ?: preferences.getColorsProvider().also { cachedColorsProvider = it } val cachedColors = cachedColorsProvider ?: preferences.getColorsProvider().also { cachedColorsProvider = it }
SessionMaterialTheme( SessionMaterialTheme(

View File

@ -102,20 +102,6 @@ Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(
}); });
} }
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_ConfigBase_merge__Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
jobject to_merge) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
std::lock_guard lock{util::util_mutex_};
auto conf = ptrToConfigBase(env, thiz);
std::vector<std::pair<std::string, session::ustring>> configs = {
extractHashAndData(env, to_merge)};
auto returned = conf->merge(configs);
auto string_stack = util::build_string_stack(env, returned);
return string_stack;
});
}
#pragma clang diagnostic pop #pragma clang diagnostic pop
} }
extern "C" extern "C"

View File

@ -30,7 +30,7 @@ Java_network_loki_messenger_libsession_1util_ConfigKt_createConfigObject(
return reinterpret_cast<jlong>(new session::config::UserProfile(secret_key, initial)); return reinterpret_cast<jlong>(new session::config::UserProfile(secret_key, initial));
} else if (config_name == "UserGroups") { } else if (config_name == "UserGroups") {
return reinterpret_cast<jlong>(new session::config::UserGroups(secret_key, initial)); return reinterpret_cast<jlong>(new session::config::UserGroups(secret_key, initial));
} else if (config_name == "ConversationVolatileConfig") { } else if (config_name == "ConvoInfoVolatile") {
return reinterpret_cast<jlong>(new session::config::ConvoInfoVolatile(secret_key, initial)); return reinterpret_cast<jlong>(new session::config::ConvoInfoVolatile(secret_key, initial));
} else { } else {
throw std::invalid_argument("Unknown config name: " + config_name); throw std::invalid_argument("Unknown config name: " + config_name);

View File

@ -2,6 +2,8 @@
#include "group_info.h" #include "group_info.h"
#include "group_members.h" #include "group_members.h"
#include "jni_utils.h"
extern "C" extern "C"
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_storageNamespace(JNIEnv* env, Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_storageNamespace(JNIEnv* env,
@ -30,7 +32,7 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newI
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;
} }
@ -165,21 +167,23 @@ extern "C"
JNIEXPORT jbyteArray JNICALL JNIEXPORT jbyteArray JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_encrypt(JNIEnv *env, jobject thiz, Java_network_loki_messenger_libsession_1util_GroupKeysConfig_encrypt(JNIEnv *env, jobject thiz,
jbyteArray plaintext) { jbyteArray plaintext) {
std::lock_guard lock{util::util_mutex_}; return jni_utils::run_catching_cxx_exception_or_throws<jbyteArray>(env, [=] {
auto ptr = ptrToKeys(env, thiz); std::lock_guard lock{util::util_mutex_};
auto plaintext_ustring = util::ustring_from_bytes(env, plaintext); auto ptr = ptrToKeys(env, thiz);
auto enc = ptr->encrypt_message(plaintext_ustring); auto plaintext_ustring = util::ustring_from_bytes(env, plaintext);
return util::bytes_from_ustring(env, enc); auto enc = ptr->encrypt_message(plaintext_ustring);
return util::bytes_from_ustring(env, enc);
});
} }
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_decrypt(JNIEnv *env, jobject thiz, Java_network_loki_messenger_libsession_1util_GroupKeysConfig_decrypt(JNIEnv *env, jobject thiz,
jbyteArray ciphertext) { jbyteArray ciphertext) {
std::lock_guard lock{util::util_mutex_}; return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
auto ptr = ptrToKeys(env, thiz); std::lock_guard lock{util::util_mutex_};
auto ciphertext_ustring = util::ustring_from_bytes(env, ciphertext); auto ptr = ptrToKeys(env, thiz);
try { auto ciphertext_ustring = util::ustring_from_bytes(env, ciphertext);
auto decrypted = ptr->decrypt_message(ciphertext_ustring); auto decrypted = ptr->decrypt_message(ciphertext_ustring);
auto sender = decrypted.first; auto sender = decrypted.first;
auto plaintext = decrypted.second; auto plaintext = decrypted.second;
@ -189,12 +193,9 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_decrypt(JNIEnv *env
auto pair_constructor = env->GetMethodID(pair_class, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V"); auto pair_constructor = env->GetMethodID(pair_class, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V");
auto pair_obj = env->NewObject(pair_class, pair_constructor, plaintext_bytes, sender_session_id); auto pair_obj = env->NewObject(pair_class, pair_constructor, plaintext_bytes, sender_session_id);
return pair_obj; return pair_obj;
} catch (std::exception& e) { });
// TODO: maybe log here
}
return nullptr;
} }
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_keys(JNIEnv *env, jobject thiz) { Java_network_loki_messenger_libsession_1util_GroupKeysConfig_keys(JNIEnv *env, jobject thiz) {

View File

@ -48,7 +48,6 @@ interface MutableConfig : ReadableConfig {
fun dump(): ByteArray fun dump(): ByteArray
fun encryptionDomain(): String fun encryptionDomain(): String
fun confirmPushed(seqNo: Long, newHash: String) fun confirmPushed(seqNo: Long, newHash: String)
fun merge(toMerge: Array<Pair<String,ByteArray>>): Stack<String>
fun dirty(): Boolean fun dirty(): Boolean
} }
@ -81,11 +80,8 @@ sealed class ConfigBase(pointer: Long): Config(pointer), MutableConfig {
external override fun dump(): ByteArray external override fun dump(): ByteArray
external override fun encryptionDomain(): String external override fun encryptionDomain(): String
external override fun confirmPushed(seqNo: Long, newHash: String) external override fun confirmPushed(seqNo: Long, newHash: String)
external override fun merge(toMerge: Array<Pair<String,ByteArray>>): Stack<String> external fun merge(toMerge: Array<Pair<String,ByteArray>>): Stack<String>
external override fun currentHashes(): List<String> external override fun currentHashes(): List<String>
// Singular merge
external fun merge(toMerge: Pair<String,ByteArray>): Stack<String>
} }

View File

@ -1,133 +1,255 @@
package org.session.libsession.messaging.configs package org.session.libsession.messaging.configs
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.MutableConfig
import network.loki.messenger.libsession_util.util.ConfigPush import network.loki.messenger.libsession_util.util.ConfigPush
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
import org.session.libsession.database.userAuth import org.session.libsession.database.userAuth
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.SwarmAuth
import org.session.libsession.snode.model.StoreMessageResponse
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.ConfigPushResult
import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsession.utilities.UserConfigType
import org.session.libsession.utilities.getClosedGroup
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
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode
import javax.inject.Inject
class ConfigSyncHandler( private const val TAG = "ConfigSyncHandler"
/**
* This class is responsible for sending the local config changes to the swarm.
*
* It does so by listening for changes in the config factory.
*/
class ConfigSyncHandler @Inject constructor(
private val configFactory: ConfigFactoryProtocol, private val configFactory: ConfigFactoryProtocol,
private val storageProtocol: StorageProtocol, private val storageProtocol: StorageProtocol,
@Suppress("OPT_IN_USAGE") scope: CoroutineScope = GlobalScope,
) { ) {
init { private var job: Job? = null
scope.launch {
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
fun start() {
require(job == null) { "Already started" }
job = GlobalScope.launch {
val groupDispatchers = hashMapOf<AccountId, CoroutineDispatcher>()
val userConfigDispatcher = Dispatchers.Default.limitedParallelism(1)
configFactory.configUpdateNotifications.collect { changes -> configFactory.configUpdateNotifications.collect { changes ->
try { try {
when (changes) { when (changes) {
is ConfigUpdateNotification.GroupConfigsDeleted -> {} is ConfigUpdateNotification.GroupConfigsDeleted -> {
is ConfigUpdateNotification.GroupConfigsUpdated -> { groupDispatchers.remove(changes.groupId)
pushGroupConfigsChangesIfNeeded(changes.groupId) }
is ConfigUpdateNotification.GroupConfigsUpdated -> {
// Group config pushing is limited to its own dispatcher
launch(groupDispatchers.getOrPut(changes.groupId) {
Dispatchers.Default.limitedParallelism(1)
}) {
pushGroupConfigsChangesIfNeeded(changes.groupId)
}
}
ConfigUpdateNotification.UserConfigs -> launch(userConfigDispatcher) {
pushUserConfigChangesIfNeeded()
} }
ConfigUpdateNotification.UserConfigs -> pushUserConfigChangesIfNeeded()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ConfigSyncHandler", "Error handling config update", e) Log.e(TAG, "Error handling config update", e)
} }
} }
} }
}
private suspend fun pushGroupConfigsChangesIfNeeded(groupId: AccountId): Unit = coroutineScope {
} }
private suspend fun pushUserConfigChangesIfNeeded(): Unit = coroutineScope { private suspend fun pushGroupConfigsChangesIfNeeded(groupId: AccountId) = coroutineScope {
// Only admin can push group configs
val adminKey = configFactory.getClosedGroup(groupId)?.adminKey
if (adminKey == null) {
Log.i(TAG, "Skipping group config push without admin key")
return@coroutineScope
}
// Gather data to push
val (membersPush, infoPush, keysPush) = configFactory.withMutableGroupConfigs(groupId) { configs ->
val membersPush = if (configs.groupMembers.needsPush()) {
configs.groupMembers.push()
} else {
null
}
val infoPush = if (configs.groupInfo.needsPush()) {
configs.groupInfo.push()
} else {
null
}
Triple(membersPush, infoPush, configs.groupKeys.pendingConfig())
}
// Nothing to push?
if (membersPush == null && infoPush == null && keysPush == null) {
return@coroutineScope
}
Log.d(TAG, "Pushing group configs")
val snode = SnodeAPI.getSingleTargetSnode(groupId.hexString).await()
val auth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey)
// Spawn the config pushing concurrently
val membersConfigHashTask = membersPush?.let {
async {
membersPush to pushConfig(
auth,
snode,
membersPush,
Namespace.CLOSED_GROUP_MEMBERS()
)
}
}
val infoConfigHashTask = infoPush?.let {
async {
infoPush to pushConfig(auth, snode, infoPush, Namespace.CLOSED_GROUP_INFO())
}
}
// Keys push is different: it doesn't have the delete call so we don't call pushConfig
val keysPushResult = keysPush?.let {
SnodeAPI.sendBatchRequest(
snode = snode,
publicKey = auth.accountId.hexString,
request = SnodeAPI.buildAuthenticatedStoreBatchInfo(
Namespace.ENCRYPTION_KEYS(),
SnodeMessage(
auth.accountId.hexString,
Base64.encodeBytes(keysPush),
SnodeMessage.CONFIG_TTL,
SnodeAPI.nowWithOffset,
),
auth
),
responseType = StoreMessageResponse::class.java
).toConfigPushResult()
}
// Wait for all other config push to come back
val memberPushResult = membersConfigHashTask?.await()
val infoPushResult = infoConfigHashTask?.await()
configFactory.confirmGroupConfigsPushed(
groupId,
memberPushResult,
infoPushResult,
keysPushResult
)
Log.i(
TAG,
"Pushed group configs, " +
"info = ${infoPush != null}, " +
"members = ${membersPush != null}, " +
"keys = ${keysPush != null}"
)
}
private suspend fun pushConfig(
auth: SwarmAuth,
snode: Snode,
push: ConfigPush,
namespace: Int
): ConfigPushResult {
val response = SnodeAPI.sendBatchRequest(
snode = snode,
publicKey = auth.accountId.hexString,
request = SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace,
SnodeMessage(
auth.accountId.hexString,
Base64.encodeBytes(push.config),
SnodeMessage.CONFIG_TTL,
SnodeAPI.nowWithOffset,
),
auth,
),
responseType = StoreMessageResponse::class.java
)
if (push.obsoleteHashes.isNotEmpty()) {
SnodeAPI.sendBatchRequest(
snode = snode,
publicKey = auth.accountId.hexString,
request = SnodeAPI.buildAuthenticatedDeleteBatchInfo(auth, push.obsoleteHashes)
)
}
return response.toConfigPushResult()
}
private suspend fun pushUserConfigChangesIfNeeded() = coroutineScope {
val userAuth = requireNotNull(storageProtocol.userAuth) { val userAuth = requireNotNull(storageProtocol.userAuth) {
"Current user not available" "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 // Gather all the user configs that need to be pushed
val pushes = configFactory.withMutableUserConfigs { configs -> val pushes = configFactory.withMutableUserConfigs { configs ->
configs.allConfigs() UserConfigType.entries
.filter { it.needsPush() } .mapNotNull { type ->
.map { config -> val config = configs.getConfig(type)
PushInformation( if (!config.needsPush()) {
namespace = config.namespace(), return@mapNotNull null
configClass = config.javaClass, }
push = config.push(),
) type to config.push()
} }
.toList()
} }
Log.d("ConfigSyncHandler", "Pushing ${pushes.size} configs") if (pushes.isEmpty()) {
return@coroutineScope
}
Log.d(TAG, "Pushing ${pushes.size} user configs")
val snode = SnodeAPI.getSingleTargetSnode(userAuth.accountId.hexString).await() val snode = SnodeAPI.getSingleTargetSnode(userAuth.accountId.hexString).await()
val pushTasks = pushes.map { info -> val pushTasks = pushes.map { (configType, configPush) ->
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 { async {
val responses = SnodeAPI.getBatchResponse( (configType to configPush) to pushConfig(userAuth, snode, configPush, configType.namespace)
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 } val pushResults = pushTasks.awaitAll().associate { it.first.first to (it.first.second to it.second) }
Log.d("ConfigSyncHandler", "Pushed ${pushResults.size} configs") Log.d(TAG, "Pushed ${pushResults.size} user configs")
configFactory.withMutableUserConfigs { configs -> configFactory.confirmUserConfigsPushed(
configs.allConfigs() contacts = pushResults[UserConfigType.CONTACTS],
.mapNotNull { config -> pushResults[config.javaClass]?.let { Triple(config, it.first, it.second) } } userGroups = pushResults[UserConfigType.USER_GROUPS],
.forEach { (config, info, hash) -> convoInfoVolatile = pushResults[UserConfigType.CONVO_INFO_VOLATILE],
config.confirmPushed(info.push.seqNo, hash) userProfile = pushResults[UserConfigType.USER_PROFILE]
} )
} }
private fun StoreMessageResponse.toConfigPushResult(): ConfigPushResult {
return ConfigPushResult(hash, timestamp)
} }
} }

View File

@ -3,9 +3,12 @@ package org.session.libsession.messaging.groups
import android.os.SystemClock import android.os.SystemClock
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.ReadableGroupKeysConfig import network.loki.messenger.libsession_util.ReadableGroupKeysConfig
@ -19,24 +22,34 @@ 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.snode.utilities.await
import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.TextSecurePreferences
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
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
import javax.inject.Inject
private const val TAG = "RemoveGroupMemberHandler" private const val TAG = "RemoveGroupMemberHandler"
private const val MIN_PROCESS_INTERVAL_MILLS = 1_000L private const val MIN_PROCESS_INTERVAL_MILLS = 1_000L
class RemoveGroupMemberHandler( class RemoveGroupMemberHandler @Inject constructor(
private val configFactory: ConfigFactoryProtocol, private val configFactory: ConfigFactoryProtocol,
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val textSecurePreferences: TextSecurePreferences,
) { ) {
init { private val scope: CoroutineScope = GlobalScope
scope.launch { private var job: Job? = null
fun start() {
require(job == null) { "Already started" }
job = scope.launch {
while (true) { while (true) {
// Make sure we have a local number before we start processing
textSecurePreferences.watchLocalNumber().first { it != null }
val processStartedAt = SystemClock.uptimeMillis() val processStartedAt = SystemClock.uptimeMillis()
try { try {

View File

@ -118,6 +118,7 @@ class JobQueue : JobDelegate {
while (isActive) { while (isActive) {
when (val job = queue.receive()) { when (val job = queue.receive()) {
is InviteContactsJob,
is NotifyPNServerJob, is NotifyPNServerJob,
is AttachmentUploadJob, is AttachmentUploadJob,
is GroupLeavingJob, is GroupLeavingJob,
@ -226,6 +227,7 @@ class JobQueue : JobDelegate {
OpenGroupDeleteJob.KEY, OpenGroupDeleteJob.KEY,
RetrieveProfileAvatarJob.KEY, RetrieveProfileAvatarJob.KEY,
GroupLeavingJob.KEY, GroupLeavingJob.KEY,
InviteContactsJob.KEY,
LibSessionGroupLeavingJob.KEY LibSessionGroupLeavingJob.KEY
) )
allJobTypes.forEach { type -> allJobTypes.forEach { type ->

View File

@ -9,7 +9,6 @@ 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.util.GroupInfo
import network.loki.messenger.libsession_util.util.Sodium import network.loki.messenger.libsession_util.util.Sodium
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.GroupManagerV2
@ -19,10 +18,13 @@ 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.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.RetrieveMessageResponse
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.ConfigMessage
import org.session.libsession.utilities.getClosedGroup
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
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
import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.Snode
@ -36,37 +38,12 @@ class ClosedGroupPoller(
private val configFactoryProtocol: ConfigFactoryProtocol, private val configFactoryProtocol: ConfigFactoryProtocol,
private val groupManagerV2: GroupManagerV2, private val groupManagerV2: GroupManagerV2,
private val storage: StorageProtocol, private val storage: StorageProtocol,
private val lokiApiDatabase: LokiAPIDatabaseProtocol,
) { ) {
data class ParsedRawMessage(
val data: ByteArray,
val hash: String,
val timestamp: Long
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParsedRawMessage
if (!data.contentEquals(other.data)) return false
if (hash != other.hash) return false
if (timestamp != other.timestamp) return false
return true
}
override fun hashCode(): Int {
var result = data.contentHashCode()
result = 31 * result + hash.hashCode()
result = 31 * result + timestamp.hashCode()
return result
}
}
companion object { companion object {
const val POLL_INTERVAL = 3_000L private const val POLL_INTERVAL = 3_000L
const val ENABLE_LOGGING = false
private const val TAG = "ClosedGroupPoller"
} }
private var job: Job? = null private var job: Job? = null
@ -74,26 +51,36 @@ class ClosedGroupPoller(
fun start() { fun start() {
if (job?.isActive == true) return // already started, don't restart if (job?.isActive == true) return // already started, don't restart
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Starting closed group poller for ${closedGroupSessionId.hexString.take(4)}") Log.d(TAG, "Starting closed group poller for ${closedGroupSessionId.hexString.take(4)}")
job?.cancel() job?.cancel()
job = scope.launch(executor) { job = scope.launch(executor) {
var snode: Snode? = null
while (isActive) { while (isActive) {
val group = configFactoryProtocol.withUserConfigs { it.userGroups.getClosedGroup(closedGroupSessionId.hexString) } ?: break configFactoryProtocol.getClosedGroup(closedGroupSessionId) ?: break
val nextPoll = runCatching { poll(group) }
if (snode == null) {
Log.i(TAG, "No Snode, fetching one")
snode = SnodeAPI.getSingleTargetSnode(closedGroupSessionId.hexString).await()
}
val nextPoll = runCatching { poll(snode!!) }
when { when {
nextPoll.isFailure -> { nextPoll.isFailure -> {
Log.e("ClosedGroupPoller", "Error polling closed group", nextPoll.exceptionOrNull()) Log.e(TAG, "Error polling closed group", nextPoll.exceptionOrNull())
// Clearing snode so we get a new one next time
snode = null
delay(POLL_INTERVAL) delay(POLL_INTERVAL)
} }
nextPoll.getOrNull() == null -> { nextPoll.getOrNull() == null -> {
// assume null poll time means don't continue polling, either the group has been deleted or something else // assume null poll time means don't continue polling, either the group has been deleted or something else
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Stopping the closed group poller") Log.d(TAG, "Stopping the closed group poller")
break break
} }
else -> { else -> {
delay(nextPoll.getOrThrow()!!) delay(POLL_INTERVAL)
} }
} }
} }
@ -105,10 +92,9 @@ class ClosedGroupPoller(
job = null job = null
} }
private suspend fun poll(group: GroupInfo.ClosedGroupInfo): Long? = coroutineScope { private suspend fun poll(snode: Snode): Unit = coroutineScope {
val snode = SnodeAPI.getSingleTargetSnode(closedGroupSessionId.hexString).await() val groupAuth =
configFactoryProtocol.getGroupAuth(closedGroupSessionId) ?: return@coroutineScope
val groupAuth = configFactoryProtocol.getGroupAuth(closedGroupSessionId) ?: return@coroutineScope null
val configHashesToExtends = configFactoryProtocol.withGroupConfigs(closedGroupSessionId) { val configHashesToExtends = configFactoryProtocol.withGroupConfigs(closedGroupSessionId) {
buildSet { buildSet {
addAll(it.groupKeys.currentHashes()) addAll(it.groupKeys.currentHashes())
@ -117,91 +103,36 @@ class ClosedGroupPoller(
} }
} }
val adminKey = requireNotNull(configFactoryProtocol.withUserConfigs { it.userGroups.getClosedGroup(closedGroupSessionId.hexString) }) { val adminKey = requireNotNull(configFactoryProtocol.withUserConfigs {
it.userGroups.getClosedGroup(closedGroupSessionId.hexString)
}) {
"Group doesn't exist" "Group doesn't exist"
}.adminKey }.adminKey
val pollingTasks = mutableListOf<Pair<String, Deferred<*>>>() val pollingTasks = mutableListOf<Pair<String, Deferred<*>>>()
pollingTasks += "Poll revoked messages" to async { pollingTasks += "retrieving revoked messages" to async {
handleRevoked( handleRevoked(
SnodeAPI.sendBatchRequest( SnodeAPI.sendBatchRequest(
snode, snode,
closedGroupSessionId.hexString, closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest( SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode, lastHash = lokiApiDatabase.getLastMessageHashValue(
snode,
closedGroupSessionId.hexString,
Namespace.REVOKED_GROUP_MESSAGES()
).orEmpty(),
auth = groupAuth, auth = groupAuth,
namespace = Namespace.REVOKED_GROUP_MESSAGES(), namespace = Namespace.REVOKED_GROUP_MESSAGES(),
maxSize = null, maxSize = null,
), ),
Map::class.java RetrieveMessageResponse::class.java
) )
) )
} }
pollingTasks += "Poll group messages" to async {
handleMessages(
body = SnodeAPI.sendBatchRequest(
snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode,
auth = groupAuth,
namespace = Namespace.CLOSED_GROUP_MESSAGES(),
maxSize = null,
),
Map::class.java),
snode = snode,
)
}
pollingTasks += "Poll group keys config" to async {
handleKeyPoll(
response = SnodeAPI.sendBatchRequest(
snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode,
auth = groupAuth,
namespace = Namespace.ENCRYPTION_KEYS(),
maxSize = null,
),
Map::class.java),
)
}
pollingTasks += "Poll group info config" to async {
handleInfo(
response = SnodeAPI.sendBatchRequest(
snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode,
auth = groupAuth,
namespace = Namespace.CLOSED_GROUP_INFO(),
maxSize = null,
),
Map::class.java),
)
}
pollingTasks += "Poll group members config" to async {
handleMembers(
SnodeAPI.sendBatchRequest(
snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode,
auth = groupAuth,
namespace = Namespace.CLOSED_GROUP_MEMBERS(),
maxSize = null,
),
Map::class.java),
)
}
if (configHashesToExtends.isNotEmpty() && adminKey != null) { if (configHashesToExtends.isNotEmpty() && adminKey != null) {
pollingTasks += "Extend group config TTL" to async { pollingTasks += "extending group config TTL" to async {
SnodeAPI.sendBatchRequest( SnodeAPI.sendBatchRequest(
snode, snode,
closedGroupSessionId.hexString, closedGroupSessionId.hexString,
@ -215,52 +146,105 @@ class ClosedGroupPoller(
} }
} }
val groupMessageRetrieval = async {
SnodeAPI.sendBatchRequest(
snode = snode,
publicKey = closedGroupSessionId.hexString,
request = SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
lastHash = lokiApiDatabase.getLastMessageHashValue(
snode,
closedGroupSessionId.hexString,
Namespace.CLOSED_GROUP_MESSAGES()
).orEmpty(),
auth = groupAuth,
namespace = Namespace.CLOSED_GROUP_MESSAGES(),
maxSize = null,
),
responseType = Map::class.java
)
}
val groupConfigRetrieval = listOf(
Namespace.ENCRYPTION_KEYS(),
Namespace.CLOSED_GROUP_INFO(),
Namespace.CLOSED_GROUP_MEMBERS()
).map { ns ->
async {
SnodeAPI.sendBatchRequest(
snode = snode,
publicKey = closedGroupSessionId.hexString,
request = SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
lastHash = lokiApiDatabase.getLastMessageHashValue(
snode,
closedGroupSessionId.hexString,
ns
).orEmpty(),
auth = groupAuth,
namespace = ns,
maxSize = null,
),
responseType = RetrieveMessageResponse::class.java
)
}
}
// The retrieval of the config and regular messages can be done concurrently,
// however, in order for the messages to be able to be decrypted, the config messages
// must be processed first.
pollingTasks += "polling and handling group config keys and messages" to async {
val (keysMessage, infoMessage, membersMessage) = groupConfigRetrieval.map { it.await() }
saveLastMessageHash(snode, keysMessage, Namespace.ENCRYPTION_KEYS())
saveLastMessageHash(snode, infoMessage, Namespace.CLOSED_GROUP_INFO())
saveLastMessageHash(snode, membersMessage, Namespace.CLOSED_GROUP_MEMBERS())
handleGroupConfigMessages(keysMessage, infoMessage, membersMessage)
val regularMessages = groupMessageRetrieval.await()
handleMessages(regularMessages, snode)
}
// Wait for all tasks to complete, gather any exceptions happened during polling
val errors = pollingTasks.mapNotNull { (name, task) -> val errors = pollingTasks.mapNotNull { (name, task) ->
runCatching { task.await() } runCatching { task.await() }
.exceptionOrNull() .exceptionOrNull()
?.takeIf { it !is CancellationException } ?.takeIf { it !is CancellationException }
?.let { RuntimeException("Error executing: $name", it) } ?.let { RuntimeException("Error $name", it) }
} }
// If there were any errors, throw the first one and add the rest as "suppressed" exceptions
if (errors.isNotEmpty()) { if (errors.isNotEmpty()) {
throw PollerException("Error polling closed group", errors) throw errors.first().apply {
} for (index in 1 until errors.size) {
addSuppressed(errors[index])
POLL_INTERVAL // this might change in future }
} }
private fun parseMessages(body: RawResponse): List<ParsedRawMessage> {
val messages = body["messages"] as? List<*> ?: return emptyList()
return messages.mapNotNull { messageMap ->
val rawMessageAsJSON = messageMap as? Map<*, *> ?: return@mapNotNull null
val base64EncodedData = rawMessageAsJSON["data"] as? String ?: return@mapNotNull null
val hash = rawMessageAsJSON["hash"] as? String ?: return@mapNotNull null
val timestamp = rawMessageAsJSON["timestamp"] as? Long ?: return@mapNotNull null
val data = base64EncodedData.let { Base64.decode(it) }
ParsedRawMessage(data, hash, timestamp)
} }
} }
private suspend fun handleRevoked(body: RawResponse) { private fun RetrieveMessageResponse.Message.toConfigMessage(): ConfigMessage {
// This shouldn't ever return null at this point return ConfigMessage(hash, data, timestamp)
val messages = body["messages"] as? List<*> }
?: return Log.w("GroupPoller", "body didn't contain a list of messages")
messages.forEach { messageMap ->
val rawMessageAsJSON = messageMap as? Map<*,*>
?: return@forEach Log.w("GroupPoller", "rawMessage wasn't a map as expected")
val data = rawMessageAsJSON["data"] as? String ?: return@forEach
val hash = rawMessageAsJSON["hash"] as? String ?: return@forEach
val timestamp = rawMessageAsJSON["timestamp"] as? Long ?: return@forEach
Log.d("GroupPoller", "Handling message with hash $hash")
private fun saveLastMessageHash(snode: Snode, body: RetrieveMessageResponse, namespace: Int) {
if (body.messages.isNotEmpty()) {
lokiApiDatabase.setLastMessageHashValue(
snode = snode,
publicKey = closedGroupSessionId.hexString,
newValue = body.messages.last().hash,
namespace = namespace
)
}
}
private suspend fun handleRevoked(body: RetrieveMessageResponse) {
body.messages.forEach { msg ->
val decoded = configFactoryProtocol.maybeDecryptForUser( val decoded = configFactoryProtocol.maybeDecryptForUser(
Base64.decode(data), msg.data,
Sodium.KICKED_DOMAIN, Sodium.KICKED_DOMAIN,
closedGroupSessionId, closedGroupSessionId,
) )
if (decoded != null) { if (decoded != null) {
Log.d("GroupPoller", "decoded kick message was for us") Log.d(TAG, "decoded kick message was for us")
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("-")
@ -279,44 +263,31 @@ class ClosedGroupPoller(
} }
} }
} }
} }
} }
private fun handleKeyPoll(response: RawResponse) { private fun handleGroupConfigMessages(
// get all the data to hash objects and process them keysResponse: RetrieveMessageResponse,
val allMessages = parseMessages(response) infoResponse: RetrieveMessageResponse,
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Total key messages this poll: ${allMessages.size}") membersResponse: RetrieveMessageResponse
var total = 0 ) {
allMessages.forEach { (message, hash, timestamp) -> if (keysResponse.messages.isEmpty() && infoResponse.messages.isEmpty() && membersResponse.messages.isEmpty()) {
configFactoryProtocol.withMutableGroupConfigs(closedGroupSessionId) { configs -> return
if (configs.loadKeys(message, hash, timestamp)) {
total++
}
}
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for keys on ${closedGroupSessionId.hexString}")
} }
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Total key messages consumed: $total")
}
private fun handleInfo(response: RawResponse) { Log.d(
val messages = parseMessages(response) TAG, "Handling group config messages(" +
messages.forEach { (message, hash, _) -> "info = ${infoResponse.messages.size}, " +
configFactoryProtocol.withMutableGroupConfigs(closedGroupSessionId) { configs -> "keys = ${keysResponse.messages.size}, " +
configs.groupInfo.merge(arrayOf(hash to message)) "members = ${membersResponse.messages.size})"
} )
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for info on ${closedGroupSessionId.hexString}")
}
}
private fun handleMembers(response: RawResponse) { configFactoryProtocol.mergeGroupConfigMessages(
parseMessages(response).forEach { (message, hash, _) -> groupId = closedGroupSessionId,
configFactoryProtocol.withMutableGroupConfigs(closedGroupSessionId) { configs -> keys = keysResponse.messages.map { it.toConfigMessage() },
configs.groupMembers.merge(arrayOf(hash to message)) info = infoResponse.messages.map { it.toConfigMessage() },
} members = membersResponse.messages.map { it.toConfigMessage() },
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for members on ${closedGroupSessionId.hexString}") )
}
} }
private fun handleMessages(body: RawResponse, snode: Snode) { private fun handleMessages(body: RawResponse, snode: Snode) {
@ -342,8 +313,8 @@ class ClosedGroupPoller(
JobQueue.shared.add(job) JobQueue.shared.add(job)
} }
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "namespace for messages rx count: ${messages.size}") if (messages.isNotEmpty()) {
Log.d(TAG, "namespace for messages rx count: ${messages.size}")
}
} }
} }

View File

@ -3,25 +3,14 @@ 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.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.MutableConfig
import network.loki.messenger.libsession_util.MutableContacts
import network.loki.messenger.libsession_util.MutableConversationVolatileConfig
import network.loki.messenger.libsession_util.MutableUserGroupsConfig
import network.loki.messenger.libsession_util.MutableUserProfile
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
import nl.komponents.kovenant.Deferred import nl.komponents.kovenant.Deferred
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.resolve import nl.komponents.kovenant.resolve
import nl.komponents.kovenant.task import nl.komponents.kovenant.task
import org.session.libsession.database.StorageProtocol
import org.session.libsession.database.userAuth import org.session.libsession.database.userAuth
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
@ -31,8 +20,9 @@ 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.ConfigMessage
import org.session.libsession.utilities.MutableGroupConfigs import org.session.libsession.utilities.UserConfigType
import org.session.libsignal.database.LokiAPIDatabaseProtocol
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
@ -46,8 +36,14 @@ private const val TAG = "Poller"
private class PromiseCanceledException : Exception("Promise canceled.") private class PromiseCanceledException : Exception("Promise canceled.")
class Poller(private val configFactory: ConfigFactoryProtocol) { class Poller(
var userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: "" private val configFactory: ConfigFactoryProtocol,
private val storage: StorageProtocol,
private val lokiApiDatabase: LokiAPIDatabaseProtocol
) {
private val userPublicKey: String
get() = storage.getUserPublicKey().orEmpty()
private var hasStarted: Boolean = false private var hasStarted: Boolean = false
private val usedSnodes: MutableSet<Snode> = mutableSetOf() private val usedSnodes: MutableSet<Snode> = mutableSetOf()
var isCaughtUp = false var isCaughtUp = false
@ -74,12 +70,14 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
} }
fun retrieveUserProfile() { fun retrieveUserProfile() {
Log.d(TAG, "Retrieving user profile.") Log.d(TAG, "Retrieving user profile. for key = $userPublicKey")
SnodeAPI.getSwarm(userPublicKey).bind { SnodeAPI.getSwarm(userPublicKey).bind {
usedSnodes.clear() usedSnodes.clear()
deferred<Unit, Exception>().also { deferred<Unit, Exception>().also {
pollNextSnode(userProfileOnly = true, it) pollNextSnode(userProfileOnly = true, it)
}.promise }.promise
}.fail {
Log.e(TAG, "Failed to retrieve user profile.", it)
} }
} }
// endregion // endregion
@ -144,8 +142,9 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
} }
} }
private fun processConfig(snode: Snode, rawMessages: RawResponse, namespace: Int, forConfig: Class<out MutableConfig>) { private fun processConfig(snode: Snode, rawMessages: RawResponse, forConfig: UserConfigType) {
val messages = rawMessages["messages"] as? List<*> val messages = rawMessages["messages"] as? List<*>
val namespace = forConfig.namespace
val processed = if (!messages.isNullOrEmpty()) { val processed = if (!messages.isNullOrEmpty()) {
SnodeAPI.updateLastMessageHashValueIfPossible(snode, userPublicKey, messages, namespace) SnodeAPI.updateLastMessageHashValueIfPossible(snode, userPublicKey, messages, namespace)
SnodeAPI.removeDuplicates(userPublicKey, messages, namespace, true).mapNotNull { rawMessageAsJSON -> SnodeAPI.removeDuplicates(userPublicKey, messages, namespace, true).mapNotNull { rawMessageAsJSON ->
@ -153,24 +152,21 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
val b64EncodedBody = rawMessageAsJSON["data"] as? String ?: return@mapNotNull null val b64EncodedBody = rawMessageAsJSON["data"] as? String ?: return@mapNotNull null
val timestamp = rawMessageAsJSON["t"] as? Long ?: SnodeAPI.nowWithOffset val timestamp = rawMessageAsJSON["t"] as? Long ?: SnodeAPI.nowWithOffset
val body = Base64.decode(b64EncodedBody) val body = Base64.decode(b64EncodedBody)
Triple(body, hashValue, timestamp) ConfigMessage(data = body, hash = hashValue, timestamp = timestamp)
} }
} else emptyList() } else emptyList()
if (processed.isEmpty()) return if (processed.isEmpty()) return
processed.forEach { (body, hash, _) -> Log.i(TAG, "Processing ${processed.size} messages for $forConfig")
try {
configFactory.withMutableUserConfigs { configs -> try {
configs configFactory.mergeUserConfigs(
.allConfigs() userConfigType = forConfig,
.filter { it.javaClass.isInstance(forConfig) } messages = processed,
.first() )
.merge(arrayOf(hash to body)) } catch (e: Exception) {
} Log.e(TAG, e)
} catch (e: Exception) {
Log.e(TAG, e)
}
} }
} }
@ -182,57 +178,57 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
} }
private fun pollUserProfile(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> = task { private fun pollUserProfile(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> = task {
runBlocking(Dispatchers.IO) { val requests = mutableListOf<SnodeAPI.SnodeBatchRequestInfo>()
val requests = mutableListOf<SnodeAPI.SnodeBatchRequestInfo>() val hashesToExtend = mutableSetOf<String>()
val hashesToExtend = mutableSetOf<String>() val userAuth = requireNotNull(MessagingModuleConfiguration.shared.storage.userAuth)
val userAuth = requireNotNull(MessagingModuleConfiguration.shared.storage.userAuth)
configFactory.withUserConfigs { configFactory.withUserConfigs {
val config = it.userProfile hashesToExtend += it.userProfile.currentHashes()
hashesToExtend += config.currentHashes() }
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode, requests += SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
lastHash = lokiApiDatabase.getLastMessageHashValue(
snode = snode,
publicKey = userAuth.accountId.hexString,
namespace = Namespace.USER_PROFILE()
),
auth = userAuth,
namespace = Namespace.USER_PROFILE(),
maxSize = -8
)
if (hashesToExtend.isNotEmpty()) {
SnodeAPI.buildAuthenticatedAlterTtlBatchRequest(
messageHashes = hashesToExtend.toList(),
auth = userAuth, auth = userAuth,
namespace = config.namespace(), newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds,
maxSize = -8 extend = true
) ).let { extensionRequest ->
}.let { request -> requests += extensionRequest
requests += request
} }
}
if (hashesToExtend.isNotEmpty()) { if (requests.isNotEmpty()) {
SnodeAPI.buildAuthenticatedAlterTtlBatchRequest( SnodeAPI.getRawBatchResponse(snode, userPublicKey, requests).bind { rawResponses ->
messageHashes = hashesToExtend.toList(), isCaughtUp = true
auth = userAuth, if (!deferred.promise.isDone()) {
newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, val responseList = (rawResponses["results"] as List<RawResponse>)
extend = true responseList.getOrNull(0)?.let { rawResponse ->
).let { extensionRequest -> if (rawResponse["code"] as? Int != 200) {
requests += extensionRequest Log.e(TAG, "Batch sub-request had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}")
} } else {
} val body = rawResponse["body"] as? RawResponse
if (body == null) {
if (requests.isNotEmpty()) { Log.e(TAG, "Batch sub-request didn't contain a body")
SnodeAPI.getRawBatchResponse(snode, userPublicKey, requests).bind { rawResponses ->
isCaughtUp = true
if (!deferred.promise.isDone()) {
val responseList = (rawResponses["results"] as List<RawResponse>)
responseList.getOrNull(0)?.let { rawResponse ->
if (rawResponse["code"] as? Int != 200) {
Log.e(TAG, "Batch sub-request had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}")
} else { } else {
val body = rawResponse["body"] as? RawResponse processConfig(snode, body, UserConfigType.USER_PROFILE)
if (body == null) {
Log.e(TAG, "Batch sub-request didn't contain a body")
} else {
processConfig(snode, body, Namespace.USER_PROFILE(), MutableUserProfile::class.java)
}
} }
} }
} }
Promise.ofSuccess(Unit)
}.fail {
Log.e(TAG, "Failed to get raw batch response", it)
} }
Promise.ofSuccess(Unit)
}.fail {
Log.e(TAG, "Failed to get raw batch response", it)
} }
} }
} }
@ -245,23 +241,37 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
val userAuth = requireNotNull(MessagingModuleConfiguration.shared.storage.userAuth) val userAuth = requireNotNull(MessagingModuleConfiguration.shared.storage.userAuth)
val requestSparseArray = SparseArray<SnodeAPI.SnodeBatchRequestInfo>() val requestSparseArray = SparseArray<SnodeAPI.SnodeBatchRequestInfo>()
// get messages // get messages
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(snode, auth = userAuth, maxSize = -2) SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
lastHash = lokiApiDatabase.getLastMessageHashValue(
snode = snode,
publicKey = userAuth.accountId.hexString,
namespace = Namespace.DEFAULT()
),
auth = userAuth,
maxSize = -2)
.also { personalMessages -> .also { personalMessages ->
// namespaces here should always be set // namespaces here should always be set
requestSparseArray[personalMessages.namespace!!] = personalMessages requestSparseArray[personalMessages.namespace!!] = personalMessages
} }
// get the latest convo info volatile // get the latest convo info volatile
val hashesToExtend = mutableSetOf<String>() val hashesToExtend = mutableSetOf<String>()
configFactory.withUserConfigs { configFactory.withUserConfigs { configs ->
it.allConfigs().map { config -> UserConfigType
hashesToExtend += config.currentHashes() .entries
config.namespace() to SnodeAPI.buildAuthenticatedRetrieveBatchRequest( .map { type ->
snode = snode, val config = configs.getConfig(type)
auth = userAuth, hashesToExtend += config.currentHashes()
namespace = config.namespace(), type.namespace to SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
maxSize = -8 lastHash = lokiApiDatabase.getLastMessageHashValue(
) snode = snode,
} publicKey = userAuth.accountId.hexString,
namespace = type.namespace
),
auth = userAuth,
namespace = type.namespace,
maxSize = -8
)
}
}.forEach { (namespace, request) -> }.forEach { (namespace, request) ->
// namespaces here should always be set // namespaces here should always be set
requestSparseArray[namespace] = request requestSparseArray[namespace] = request
@ -290,29 +300,24 @@ 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
sequenceOf( UserConfigType.entries
Namespace.USER_PROFILE() to MutableUserProfile::class.java, .map { type -> type to requestSparseArray.indexOfKey(type.namespace) }
Namespace.CONTACTS() to MutableContacts::class.java, .filter { (_, i) -> i >= 0 }
Namespace.GROUPS() to MutableUserGroupsConfig::class.java, .forEach { (configType, requestIndex) ->
Namespace.CONVO_INFO_VOLATILE() to MutableConversationVolatileConfig::class.java responseList.getOrNull(requestIndex)?.let { rawResponse ->
).map { (namespace, configClass) -> if (rawResponse["code"] as? Int != 200) {
Triple(namespace, configClass, requestSparseArray.indexOfKey(namespace)) Log.e(TAG, "Batch sub-request had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}")
}.filter { (_, _, i) -> i >= 0 } return@forEach
.forEach { (namespace, configClass, requestIndex) -> }
responseList.getOrNull(requestIndex)?.let { rawResponse -> val body = rawResponse["body"] as? RawResponse
if (rawResponse["code"] as? Int != 200) { if (body == null) {
Log.e(TAG, "Batch sub-request had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}") Log.e(TAG, "Batch sub-request didn't contain a body")
return@forEach return@forEach
} }
val body = rawResponse["body"] as? RawResponse
if (body == null) {
Log.e(TAG, "Batch sub-request didn't contain a body")
return@forEach
}
processConfig(snode, body, namespace, configClass) processConfig(snode, body, configType)
}
} }
}
// the first response will be the personal messages (we want these to be processed after config messages) // the first response will be the personal messages (we want these to be processed after config messages)
val personalResponseIndex = requestSparseArray.indexOfKey(Namespace.DEFAULT()) val personalResponseIndex = requestSparseArray.indexOfKey(Namespace.DEFAULT())

View File

@ -1,15 +0,0 @@
package org.session.libsession.messaging.sending_receiving.pollers
/**
* Exception thrown by a Poller-family when multiple error could have occurred.
*/
class PollerException(message: String, errors: List<Throwable>) : RuntimeException(
message,
errors.firstOrNull()
) {
init {
errors.asSequence()
.drop(1)
.forEach(this::addSuppressed)
}
}

View File

@ -192,7 +192,7 @@ object UpdateMessageBuilder {
.put(OTHER_NAME_KEY, context.youOrSender(updateData.sessionIds.first { it != userPublicKey })) .put(OTHER_NAME_KEY, context.youOrSender(updateData.sessionIds.first { it != userPublicKey }))
.format() .format()
number == 2 -> Phrase.from(context, number == 2 -> Phrase.from(context,
R.string.groupMemberNewMultiple) R.string.groupMemberNewTwo)
.put(NAME_KEY, context.youOrSender(updateData.sessionIds.first())) .put(NAME_KEY, context.youOrSender(updateData.sessionIds.first()))
.put(OTHER_NAME_KEY, context.youOrSender(updateData.sessionIds.last())) .put(OTHER_NAME_KEY, context.youOrSender(updateData.sessionIds.last()))
.format() .format()

View File

@ -510,8 +510,8 @@ object SnodeAPI {
} }
fun buildAuthenticatedRetrieveBatchRequest( fun buildAuthenticatedRetrieveBatchRequest(
snode: Snode,
auth: SwarmAuth, auth: SwarmAuth,
lastHash: String?,
namespace: Int = 0, namespace: Int = 0,
maxSize: Int? = null maxSize: Int? = null
): SnodeBatchRequestInfo { ): SnodeBatchRequestInfo {
@ -520,7 +520,7 @@ object SnodeAPI {
auth = auth, auth = auth,
verificationData = { ns, t -> "${Snode.Method.Retrieve.rawValue}$ns$t" }, verificationData = { ns, t -> "${Snode.Method.Retrieve.rawValue}$ns$t" },
) { ) {
put("last_hash", database.getLastMessageHashValue(snode, auth.accountId.hexString, namespace).orEmpty()) put("last_hash", lastHash.orEmpty())
if (maxSize != null) { if (maxSize != null) {
put("max_size", maxSize) put("max_size", maxSize)
} }
@ -639,13 +639,15 @@ object SnodeAPI {
return@batch return@batch
} }
// For each response, parse the result, match it with the request then send
// back through the request's callback.
for ((req, resp) in batch.zip(responses.results)) { for ((req, resp) in batch.zip(responses.results)) {
val result = if (resp.code != 200) { val result = runCatching {
Result.failure(RuntimeException("Error with code = ${resp.code}, msg = ${resp.body}")) check(resp.code == 200) {
} else { "Error with code = ${resp.code}, msg = ${resp.body}"
runCatching {
JsonUtil.fromJson(resp.body, req.responseType)
} }
JsonUtil.fromJson(resp.body, req.responseType)
} }
req.callback.send(result) req.callback.send(result)

View File

@ -0,0 +1,20 @@
package org.session.libsession.snode.model
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
data class StoreMessageResponse @JsonCreator constructor(
@JsonProperty("hash") val hash: String,
@JsonProperty("t") val timestamp: Long,
)
class RetrieveMessageResponse @JsonCreator constructor(
@JsonProperty("messages") val messages: List<Message>,
) {
class Message @JsonCreator constructor(
@JsonProperty("hash") val hash: String,
@JsonProperty("t") val timestamp: Long,
// Jackson is able to deserialize byte arrays from base64 strings
@JsonProperty("data") val data: ByteArray,
)
}

View File

@ -1,9 +0,0 @@
package org.session.libsession.snode.model
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
data class StoreMessageResponse @JsonCreator constructor(
@JsonProperty("hash") val hash: String,
@JsonProperty("t") val timestamp: Long,
)

View File

@ -17,14 +17,18 @@ import network.loki.messenger.libsession_util.ReadableGroupKeysConfig
import network.loki.messenger.libsession_util.ReadableGroupMembersConfig import network.loki.messenger.libsession_util.ReadableGroupMembersConfig
import network.loki.messenger.libsession_util.ReadableUserGroupsConfig import network.loki.messenger.libsession_util.ReadableUserGroupsConfig
import network.loki.messenger.libsession_util.ReadableUserProfile import network.loki.messenger.libsession_util.ReadableUserProfile
import network.loki.messenger.libsession_util.util.ConfigPush
import network.loki.messenger.libsession_util.util.GroupInfo
import org.session.libsession.snode.SwarmAuth import org.session.libsession.snode.SwarmAuth
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Namespace
interface ConfigFactoryProtocol { interface ConfigFactoryProtocol {
val configUpdateNotifications: Flow<ConfigUpdateNotification> val configUpdateNotifications: Flow<ConfigUpdateNotification>
fun <T> withUserConfigs(cb: (UserConfigs) -> T): T fun <T> withUserConfigs(cb: (UserConfigs) -> T): T
fun <T> withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T fun <T> withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T
fun mergeUserConfigs(userConfigType: UserConfigType, messages: List<ConfigMessage>)
fun <T> withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T fun <T> withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T
fun <T> withMutableGroupConfigs(groupId: AccountId, cb: (MutableGroupConfigs) -> T): T fun <T> withMutableGroupConfigs(groupId: AccountId, cb: (MutableGroupConfigs) -> T): T
@ -39,8 +43,52 @@ interface ConfigFactoryProtocol {
domain: String, domain: String,
closedGroupSessionId: AccountId): ByteArray? closedGroupSessionId: AccountId): ByteArray?
fun mergeGroupConfigMessages(
groupId: AccountId,
keys: List<ConfigMessage>,
info: List<ConfigMessage>,
members: List<ConfigMessage>
)
fun confirmUserConfigsPushed(
contacts: Pair<ConfigPush, ConfigPushResult>? = null,
userProfile: Pair<ConfigPush, ConfigPushResult>? = null,
convoInfoVolatile: Pair<ConfigPush, ConfigPushResult>? = null,
userGroups: Pair<ConfigPush, ConfigPushResult>? = null
)
fun confirmGroupConfigsPushed(
groupId: AccountId,
members: Pair<ConfigPush, ConfigPushResult>?,
info: Pair<ConfigPush, ConfigPushResult>?,
keysPush: ConfigPushResult?
)
} }
class ConfigMessage(
val hash: String,
val data: ByteArray,
val timestamp: Long
)
data class ConfigPushResult(
val hash: String,
val timestamp: Long
)
enum class UserConfigType(val namespace: Int) {
CONTACTS(Namespace.CONTACTS()),
USER_PROFILE(Namespace.USER_PROFILE()),
CONVO_INFO_VOLATILE(Namespace.CONVO_INFO_VOLATILE()),
USER_GROUPS(Namespace.GROUPS()),
}
/**
* Shortcut to get the group info for a closed group. Equivalent to: `withUserConfigs { it.userGroups.getClosedGroup(groupId) }`
*/
fun ConfigFactoryProtocol.getClosedGroup(groupId: AccountId): GroupInfo.ClosedGroupInfo? {
return withUserConfigs { it.userGroups.getClosedGroup(groupId.hexString) }
}
interface UserConfigs { interface UserConfigs {
val contacts: ReadableContacts val contacts: ReadableContacts
@ -48,7 +96,14 @@ interface UserConfigs {
val userProfile: ReadableUserProfile val userProfile: ReadableUserProfile
val convoInfoVolatile: ReadableConversationVolatileConfig val convoInfoVolatile: ReadableConversationVolatileConfig
fun allConfigs(): Sequence<ReadableConfig> = sequenceOf(contacts, userGroups, userProfile, convoInfoVolatile) fun getConfig(type: UserConfigType): ReadableConfig {
return when (type) {
UserConfigType.CONTACTS -> contacts
UserConfigType.USER_PROFILE -> userProfile
UserConfigType.CONVO_INFO_VOLATILE -> convoInfoVolatile
UserConfigType.USER_GROUPS -> userGroups
}
}
} }
interface MutableUserConfigs : UserConfigs { interface MutableUserConfigs : UserConfigs {
@ -57,7 +112,14 @@ interface MutableUserConfigs : UserConfigs {
override val userProfile: MutableUserProfile override val userProfile: MutableUserProfile
override val convoInfoVolatile: MutableConversationVolatileConfig override val convoInfoVolatile: MutableConversationVolatileConfig
override fun allConfigs(): Sequence<MutableConfig> = sequenceOf(contacts, userGroups, userProfile, convoInfoVolatile) override fun getConfig(type: UserConfigType): MutableConfig {
return when (type) {
UserConfigType.CONTACTS -> contacts
UserConfigType.USER_PROFILE -> userProfile
UserConfigType.CONVO_INFO_VOLATILE -> convoInfoVolatile
UserConfigType.USER_GROUPS -> userGroups
}
}
} }
interface GroupConfigs { interface GroupConfigs {
@ -71,8 +133,7 @@ interface MutableGroupConfigs : GroupConfigs {
override val groupMembers: MutableGroupMembersConfig override val groupMembers: MutableGroupMembersConfig
override val groupKeys: MutableGroupKeysConfig override val groupKeys: MutableGroupKeysConfig
fun loadKeys(message: ByteArray, hash: String, timestamp: Long): Boolean fun rekey()
fun rekeys()
} }
sealed interface ConfigUpdateNotification { sealed interface ConfigUpdateNotification {
@ -80,30 +141,3 @@ sealed interface ConfigUpdateNotification {
data class GroupConfigsUpdated(val groupId: AccountId) : ConfigUpdateNotification data class GroupConfigsUpdated(val groupId: AccountId) : ConfigUpdateNotification
data class GroupConfigsDeleted(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
//}

View File

@ -41,8 +41,6 @@ import javax.inject.Singleton
interface TextSecurePreferences { interface TextSecurePreferences {
fun getLastConfigurationSyncTime(): Long
fun setLastConfigurationSyncTime(value: Long)
fun getConfigurationMessageSynced(): Boolean fun getConfigurationMessageSynced(): Boolean
fun setConfigurationMessageSynced(value: Boolean) fun setConfigurationMessageSynced(value: Boolean)
@ -108,6 +106,7 @@ interface TextSecurePreferences {
fun setUpdateApkDigest(value: String?) fun setUpdateApkDigest(value: String?)
fun getUpdateApkDigest(): String? fun getUpdateApkDigest(): String?
fun getLocalNumber(): String? fun getLocalNumber(): String?
fun watchLocalNumber(): StateFlow<String?>
fun getHasLegacyConfig(): Boolean fun getHasLegacyConfig(): Boolean
fun setHasLegacyConfig(newValue: Boolean) fun setHasLegacyConfig(newValue: Boolean)
fun setLocalNumber(localNumber: String) fun setLocalNumber(localNumber: String)
@ -203,6 +202,10 @@ interface TextSecurePreferences {
@JvmStatic @JvmStatic
var pushSuffix = "" var pushSuffix = ""
// This is a stop-gap solution for static access to shared preference.
internal lateinit var preferenceInstance: TextSecurePreferences
const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase" const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase"
const val LANGUAGE_PREF = "pref_language" const val LANGUAGE_PREF = "pref_language"
const val THREAD_TRIM_NOW = "pref_trim_now" const val THREAD_TRIM_NOW = "pref_trim_now"
@ -219,7 +222,7 @@ interface TextSecurePreferences {
const val SCREEN_SECURITY_PREF = "pref_screen_security" const val SCREEN_SECURITY_PREF = "pref_screen_security"
const val ENTER_SENDS_PREF = "pref_enter_sends" const val ENTER_SENDS_PREF = "pref_enter_sends"
const val THREAD_TRIM_ENABLED = "pref_trim_threads" const val THREAD_TRIM_ENABLED = "pref_trim_threads"
const val LOCAL_NUMBER_PREF = "pref_local_number" internal const val LOCAL_NUMBER_PREF = "pref_local_number"
const val REGISTERED_GCM_PREF = "pref_gcm_registered" const val REGISTERED_GCM_PREF = "pref_gcm_registered"
const val UPDATE_APK_REFRESH_TIME_PREF = "pref_update_apk_refresh_time" const val UPDATE_APK_REFRESH_TIME_PREF = "pref_update_apk_refresh_time"
const val UPDATE_APK_DOWNLOAD_ID = "pref_update_apk_download_id" const val UPDATE_APK_DOWNLOAD_ID = "pref_update_apk_download_id"
@ -265,7 +268,6 @@ interface TextSecurePreferences {
const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning" const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning"
const val GIF_GRID_LAYOUT = "pref_gif_grid_layout" const val GIF_GRID_LAYOUT = "pref_gif_grid_layout"
val IS_PUSH_ENABLED get() = "pref_is_using_fcm$pushSuffix" val IS_PUSH_ENABLED get() = "pref_is_using_fcm$pushSuffix"
const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
const val CONFIGURATION_SYNCED = "pref_configuration_synced" const val CONFIGURATION_SYNCED = "pref_configuration_synced"
const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time" const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
const val LAST_OPEN_DATE = "pref_last_open_date" const val LAST_OPEN_DATE = "pref_last_open_date"
@ -308,16 +310,16 @@ interface TextSecurePreferences {
// for the lifetime of the Session installation. // for the lifetime of the Session installation.
const val HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS = "libsession.HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS" const val HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS = "libsession.HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS"
@JvmStatic // @JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long { // fun getLastConfigurationSyncTime(context: Context): Long {
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0) // return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
} // }
//
@JvmStatic // @JvmStatic
fun setLastConfigurationSyncTime(context: Context, value: Long) { // fun setLastConfigurationSyncTime(context: Context, value: Long) {
setLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, value) // setLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, value)
} // }
//
@JvmStatic @JvmStatic
fun getConfigurationMessageSynced(context: Context): Boolean { fun getConfigurationMessageSynced(context: Context): Boolean {
return getBooleanPreference(context, CONFIGURATION_SYNCED, false) return getBooleanPreference(context, CONFIGURATION_SYNCED, false)
@ -629,9 +631,13 @@ interface TextSecurePreferences {
return getStringPreference(context, UPDATE_APK_DIGEST, null) return getStringPreference(context, UPDATE_APK_DIGEST, null)
} }
@Deprecated(
"Use the dependency-injected TextSecurePreference instance instead",
ReplaceWith("TextSecurePreferences.getLocalNumber()")
)
@JvmStatic @JvmStatic
fun getLocalNumber(context: Context): String? { fun getLocalNumber(context: Context): String? {
return getStringPreference(context, LOCAL_NUMBER_PREF, null) return preferenceInstance.getLocalNumber()
} }
@JvmStatic @JvmStatic
@ -645,13 +651,6 @@ interface TextSecurePreferences {
_events.tryEmit(HAS_RECEIVED_LEGACY_CONFIG) _events.tryEmit(HAS_RECEIVED_LEGACY_CONFIG)
} }
fun setLocalNumber(context: Context, localNumber: String) {
setStringPreference(context, LOCAL_NUMBER_PREF, localNumber.toLowerCase())
}
fun removeLocalNumber(context: Context) {
removePreference(context, LOCAL_NUMBER_PREF)
}
@JvmStatic @JvmStatic
fun isEnterSendsEnabled(context: Context): Boolean { fun isEnterSendsEnabled(context: Context): Boolean {
@ -994,14 +993,12 @@ interface TextSecurePreferences {
class AppTextSecurePreferences @Inject constructor( class AppTextSecurePreferences @Inject constructor(
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
): TextSecurePreferences { ): TextSecurePreferences {
init {
override fun getLastConfigurationSyncTime(): Long { // Should remove once all static access to the companion objects is removed
return getLongPreference(TextSecurePreferences.LAST_CONFIGURATION_SYNC_TIME, 0) TextSecurePreferences.preferenceInstance = this
} }
override fun setLastConfigurationSyncTime(value: Long) { private val localNumberState = MutableStateFlow(getStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, null))
setLongPreference(TextSecurePreferences.LAST_CONFIGURATION_SYNC_TIME, value)
}
override fun getConfigurationMessageSynced(): Boolean { override fun getConfigurationMessageSynced(): Boolean {
return getBooleanPreference(TextSecurePreferences.CONFIGURATION_SYNCED, false) return getBooleanPreference(TextSecurePreferences.CONFIGURATION_SYNCED, false)
@ -1262,7 +1259,11 @@ class AppTextSecurePreferences @Inject constructor(
} }
override fun getLocalNumber(): String? { override fun getLocalNumber(): String? {
return getStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, null) return localNumberState.value
}
override fun watchLocalNumber(): StateFlow<String?> {
return localNumberState
} }
override fun getHasLegacyConfig(): Boolean { override fun getHasLegacyConfig(): Boolean {
@ -1275,10 +1276,13 @@ class AppTextSecurePreferences @Inject constructor(
} }
override fun setLocalNumber(localNumber: String) { override fun setLocalNumber(localNumber: String) {
setStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, localNumber.toLowerCase()) val normalised = localNumber.lowercase()
setStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, normalised)
localNumberState.value = normalised
} }
override fun removeLocalNumber() { override fun removeLocalNumber() {
localNumberState.value = null
removePreference(TextSecurePreferences.LOCAL_NUMBER_PREF) removePreference(TextSecurePreferences.LOCAL_NUMBER_PREF)
} }