diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt index b17fde5c33..86af15d5ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -27,7 +27,7 @@ import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.snode.OwnedSwarmAuth -import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeClock import org.session.libsession.snode.SwarmAuth import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol @@ -52,7 +52,6 @@ import org.thoughtcrime.securesms.database.ConfigDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.groups.GroupManager -import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -64,163 +63,23 @@ class ConfigFactory @Inject constructor( private val threadDb: ThreadDatabase, private val lokiThreadDatabase: LokiThreadDatabase, private val storage: Lazy, - private val textSecurePreferences: TextSecurePreferences + private val textSecurePreferences: TextSecurePreferences, + private val clock: SnodeClock, ) : ConfigFactoryProtocol { companion object { // This is a buffer period within which we will process messages which would result in a // config change, any message which would normally result in a config change which was sent // before `lastConfigMessage.timestamp - configChangeBufferPeriod` will not actually have // it's changes applied (control text will still be added though) - const val configChangeBufferPeriod: Long = (2 * 60 * 1000) + private const val CONFIG_CHANGE_BUFFER_PERIOD: Long = 2 * 60 * 1000L } init { System.loadLibrary("session_util") } - private class UserConfigsImpl( - userEd25519SecKey: ByteArray, - private val userAccountId: AccountId, - private val configDatabase: ConfigDatabase, - storage: StorageProtocol, - threadDb: ThreadDatabase, - contactsDump: ByteArray? = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.CONTACTS_VARIANT, - userAccountId.hexString - ), - userGroupsDump: ByteArray? = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.USER_GROUPS_VARIANT, - userAccountId.hexString - ), - userProfileDump: ByteArray? = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.USER_PROFILE_VARIANT, - userAccountId.hexString - ), - convoInfoDump: ByteArray? = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.CONVO_INFO_VARIANT, - userAccountId.hexString - ) - ) : MutableUserConfigs { - override val contacts = Contacts( - ed25519SecretKey = userEd25519SecKey, - initialDump = contactsDump, - ) - - override val userGroups = UserGroupsConfig( - ed25519SecretKey = userEd25519SecKey, - initialDump = userGroupsDump - ) - override val userProfile = UserProfile( - ed25519SecretKey = userEd25519SecKey, - initialDump = userProfileDump - ) - override val convoInfoVolatile = ConversationVolatileConfig( - ed25519SecretKey = userEd25519SecKey, - initialDump = convoInfoDump, - ) - - init { - if (contactsDump == null) { - contacts.initFrom(storage) - } - - if (userGroupsDump == null) { - userGroups.initFrom(storage) - } - - if (userProfileDump == null) { - userProfile.initFrom(storage) - } - - if (convoInfoDump == null) { - convoInfoVolatile.initFrom(storage, threadDb) - } - } - - /** - * Persists the config if it is dirty and returns the list of classes that were persisted - */ - fun persistIfDirty(): Boolean { - return sequenceOf( - contacts to ConfigDatabase.CONTACTS_VARIANT, - userGroups to ConfigDatabase.USER_GROUPS_VARIANT, - userProfile to ConfigDatabase.USER_PROFILE_VARIANT, - convoInfoVolatile to ConfigDatabase.CONVO_INFO_VARIANT - ).fold(false) { acc, (config, variant) -> - if (config.needsDump()) { - configDatabase.storeConfig( - variant = variant, - publicKey = userAccountId.hexString, - data = config.dump(), - timestamp = SnodeAPI.nowWithOffset - ) - true - } else { - acc - } - } - } - } - - private class GroupConfigsImpl( - userEd25519SecKey: ByteArray, - private val groupAccountId: AccountId, - groupAdminKey: ByteArray?, - private val configDatabase: ConfigDatabase - ) : MutableGroupConfigs { - override val groupInfo = GroupInfoConfig( - groupPubKey = groupAccountId.pubKeyBytes, - groupAdminKey = groupAdminKey, - initialDump = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.INFO_VARIANT, - groupAccountId.hexString - ) - ) - override val groupMembers = GroupMembersConfig( - groupPubKey = groupAccountId.pubKeyBytes, - groupAdminKey = groupAdminKey, - initialDump = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.MEMBER_VARIANT, - groupAccountId.hexString - ) - ) - override val groupKeys = GroupKeysConfig( - userSecretKey = userEd25519SecKey, - groupPublicKey = groupAccountId.pubKeyBytes, - groupAdminKey = groupAdminKey, - initialDump = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.KEYS_VARIANT, - groupAccountId.hexString - ), - info = groupInfo, - members = groupMembers - ) - - fun dumpIfNeeded(): Boolean { - if (groupInfo.needsDump() || groupMembers.needsDump() || groupKeys.needsDump()) { - configDatabase.storeGroupConfigs( - publicKey = groupAccountId.hexString, - keysConfig = groupKeys.dump(), - infoConfig = groupInfo.dump(), - memberConfig = groupMembers.dump(), - timestamp = SnodeAPI.nowWithOffset - ) - return true - } - - return false - } - - val isDirty: Boolean - get() = groupInfo.dirty() || groupMembers.dirty() - - override fun rekey() { - groupKeys.rekey(groupInfo.pointer, groupMembers.pointer) - } - } - - private val userConfigs = ConcurrentHashMap() - private val groupConfigs = ConcurrentHashMap() + private val userConfigs = HashMap() + private val groupConfigs = HashMap() private val _configUpdateNotifications = MutableSharedFlow( extraBufferCapacity = 5, // The notifications are normally important so we can afford to buffer a few @@ -240,14 +99,16 @@ class ConfigFactory @Inject constructor( override fun withUserConfigs(cb: (UserConfigs) -> T): T { val userAccountId = requiresCurrentUserAccountId() - val configs = userConfigs.getOrPut(userAccountId) { - UserConfigsImpl( - requiresCurrentUserED25519SecKey(), - userAccountId, - threadDb = threadDb, - configDatabase = configDatabase, - storage = storage.get() - ) + val configs = synchronized(userConfigs) { + userConfigs.getOrPut(userAccountId) { + UserConfigsImpl( + requiresCurrentUserED25519SecKey(), + userAccountId, + threadDb = threadDb, + configDatabase = configDatabase, + storage = storage.get() + ) + } } return synchronized(configs) { @@ -294,20 +155,27 @@ class ConfigFactory @Inject constructor( override fun withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T { return doWithMutableUserConfigs { - cb(it) to it.persistIfDirty() + cb(it) to it.persistIfDirty(clock) } } override fun withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T { - val configs = groupConfigs.getOrPut(groupId) { - val groupAdminKey = getClosedGroup(groupId)?.adminKey + val groupAdminKey = getClosedGroup(groupId)?.adminKey - GroupConfigsImpl( - requiresCurrentUserED25519SecKey(), - groupId, - groupAdminKey, - configDatabase - ) + val configs = synchronized(groupConfigs) { + var value = groupConfigs[groupId] + if (value == null || value.groupKeys.admin() != (groupAdminKey != null)) { + // No existing configs or existing configs have different admin settings with what we currently have + // Create a new group configs + value = GroupConfigsImpl( + requiresCurrentUserED25519SecKey(), + groupId, + groupAdminKey, + configDatabase + ).also { groupConfigs[groupId] = it } + } + + value } return synchronized(configs) { @@ -342,7 +210,7 @@ class ConfigFactory @Inject constructor( cb: (MutableGroupConfigs) -> T ): T { return doWithMutableGroupConfigs(recreateConfigInstances = recreateConfigInstances, groupId = groupId) { - cb(it) to it.dumpIfNeeded() + cb(it) to it.dumpIfNeeded(clock) } } @@ -359,7 +227,7 @@ class ConfigFactory @Inject constructor( configDatabase.deleteGroupConfigs(groupId) } - override fun maybeDecryptForUser( + override fun decryptForUser( encoded: ByteArray, domain: String, closedGroupSessionId: AccountId @@ -392,7 +260,7 @@ class ConfigFactory @Inject constructor( val membersMerged = members.isNotEmpty() && configs.groupMembers.merge(members.map { it.hash to it.data }.toTypedArray()).isNotEmpty() - configs.dumpIfNeeded() + configs.dumpIfNeeded(clock) Unit to (keysLoaded || infoMerged || membersMerged) } @@ -414,7 +282,7 @@ class ConfigFactory @Inject constructor( 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() + Unit to configs.persistIfDirty(clock) } } @@ -438,7 +306,7 @@ class ConfigFactory @Inject constructor( } } - Unit to configs.dumpIfNeeded() + Unit to configs.dumpIfNeeded(clock) } } @@ -489,7 +357,7 @@ class ConfigFactory @Inject constructor( configDatabase.retrieveConfigLastUpdateTimestamp(variant, publicKey) // Ensure the change occurred after the last config message was handled (minus the buffer period) - return (changeTimestampMs >= (lastUpdateTimestampMs - configChangeBufferPeriod)) + return (changeTimestampMs >= (lastUpdateTimestampMs - CONFIG_CHANGE_BUFFER_PERIOD)) } override fun getGroupAuth(groupId: AccountId): SwarmAuth? { @@ -680,4 +548,142 @@ private fun MutableContacts.initFrom(storage: StorageProtocol) { ) set(contactInfo) } +} + +private class UserConfigsImpl( + userEd25519SecKey: ByteArray, + private val userAccountId: AccountId, + private val configDatabase: ConfigDatabase, + storage: StorageProtocol, + threadDb: ThreadDatabase, + contactsDump: ByteArray? = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.CONTACTS_VARIANT, + userAccountId.hexString + ), + userGroupsDump: ByteArray? = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.USER_GROUPS_VARIANT, + userAccountId.hexString + ), + userProfileDump: ByteArray? = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.USER_PROFILE_VARIANT, + userAccountId.hexString + ), + convoInfoDump: ByteArray? = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.CONVO_INFO_VARIANT, + userAccountId.hexString + ) +) : MutableUserConfigs { + override val contacts = Contacts( + ed25519SecretKey = userEd25519SecKey, + initialDump = contactsDump, + ) + + override val userGroups = UserGroupsConfig( + ed25519SecretKey = userEd25519SecKey, + initialDump = userGroupsDump + ) + override val userProfile = UserProfile( + ed25519SecretKey = userEd25519SecKey, + initialDump = userProfileDump + ) + override val convoInfoVolatile = ConversationVolatileConfig( + ed25519SecretKey = userEd25519SecKey, + initialDump = convoInfoDump, + ) + + init { + if (contactsDump == null) { + contacts.initFrom(storage) + } + + if (userGroupsDump == null) { + userGroups.initFrom(storage) + } + + if (userProfileDump == null) { + userProfile.initFrom(storage) + } + + if (convoInfoDump == null) { + convoInfoVolatile.initFrom(storage, threadDb) + } + } + + /** + * Persists the config if it is dirty and returns the list of classes that were persisted + */ + fun persistIfDirty(clock: SnodeClock): Boolean { + return sequenceOf( + contacts to ConfigDatabase.CONTACTS_VARIANT, + userGroups to ConfigDatabase.USER_GROUPS_VARIANT, + userProfile to ConfigDatabase.USER_PROFILE_VARIANT, + convoInfoVolatile to ConfigDatabase.CONVO_INFO_VARIANT + ).fold(false) { acc, (config, variant) -> + if (config.needsDump()) { + configDatabase.storeConfig( + variant = variant, + publicKey = userAccountId.hexString, + data = config.dump(), + timestamp = clock.currentTimeMills() + ) + true + } else { + acc + } + } + } +} + +private class GroupConfigsImpl( + userEd25519SecKey: ByteArray, + private val groupAccountId: AccountId, + groupAdminKey: ByteArray?, + private val configDatabase: ConfigDatabase +) : MutableGroupConfigs { + override val groupInfo = GroupInfoConfig( + groupPubKey = groupAccountId.pubKeyBytes, + groupAdminKey = groupAdminKey, + initialDump = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.INFO_VARIANT, + groupAccountId.hexString + ) + ) + override val groupMembers = GroupMembersConfig( + groupPubKey = groupAccountId.pubKeyBytes, + groupAdminKey = groupAdminKey, + initialDump = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.MEMBER_VARIANT, + groupAccountId.hexString + ) + ) + override val groupKeys = GroupKeysConfig( + userSecretKey = userEd25519SecKey, + groupPublicKey = groupAccountId.pubKeyBytes, + groupAdminKey = groupAdminKey, + initialDump = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.KEYS_VARIANT, + groupAccountId.hexString + ), + info = groupInfo, + members = groupMembers + ) + + fun dumpIfNeeded(clock: SnodeClock): Boolean { + if (groupInfo.needsDump() || groupMembers.needsDump() || groupKeys.needsDump()) { + configDatabase.storeGroupConfigs( + publicKey = groupAccountId.hexString, + keysConfig = groupKeys.dump(), + infoConfig = groupInfo.dump(), + memberConfig = groupMembers.dump(), + timestamp = clock.currentTimeMills() + ) + return true + } + + return false + } + + override fun rekey() { + groupKeys.rekey(groupInfo.pointer, groupMembers.pointer) + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/PollerFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/PollerFactory.kt index 11ed27dddc..305d298092 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/PollerFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/PollerFactory.kt @@ -7,6 +7,7 @@ import network.loki.messenger.libsession_util.util.GroupInfo import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller +import org.session.libsession.snode.SnodeClock import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.AccountId import java.util.concurrent.ConcurrentHashMap @@ -18,6 +19,7 @@ class PollerFactory( private val groupManagerV2: Lazy, private val storage: Lazy, private val lokiApiDatabase: LokiAPIDatabaseProtocol, + private val clock: SnodeClock, ) { private val pollers = ConcurrentHashMap() @@ -39,6 +41,7 @@ class PollerFactory( groupManagerV2 = groupManagerV2.get(), storage = storage.get(), lokiApiDatabase = lokiApiDatabase, + clock = clock, ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt index c6c513fad5..56e0012da8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt @@ -45,13 +45,15 @@ object SessionUtilModule { configFactory: ConfigFactory, storage: Lazy, groupManagerV2: Lazy, - lokiApiDatabase: LokiAPIDatabaseProtocol) = PollerFactory( + lokiApiDatabase: LokiAPIDatabaseProtocol, + clock: SnodeClock) = PollerFactory( scope = coroutineScope, executor = dispatcher, configFactory = configFactory, groupManagerV2 = groupManagerV2, storage = storage, lokiApiDatabase = lokiApiDatabase, + clock = clock, ) @Provides diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index 0ff21b5e11..302a9b0a18 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -554,7 +554,8 @@ class GroupManagerV2Impl @Inject constructor( // this will fail the first couple of times :) MessageSender.send( responseMessage, - Address.fromSerialized(group.groupAccountId.hexString) + Destination.ClosedGroup(group.groupAccountId.hexString), + isSyncMessage = false ) } else { // If we are invited as admin, we can just update the group info ourselves @@ -756,7 +757,7 @@ class GroupManagerV2Impl @Inject constructor( } storage.insertIncomingInfoMessage( - context = MessagingModuleConfiguration.shared.context, + context = application, senderPublicKey = userId, groupID = groupId.hexString, type = SignalServiceGroup.Type.KICKED, diff --git a/libsession-util/libsession-util b/libsession-util/libsession-util index 995e22dcbf..f649dec5d6 160000 --- a/libsession-util/libsession-util +++ b/libsession-util/libsession-util @@ -1 +1 @@ -Subproject commit 995e22dcbf08b3cb9e2ad595859e4cd9a4ed8776 +Subproject commit f649dec5d6a38365e3add8fe0b4c159b2ffe19ac diff --git a/libsession-util/src/main/cpp/group_keys.cpp b/libsession-util/src/main/cpp/group_keys.cpp index 03ab3ff2fa..31d6a4fe5a 100644 --- a/libsession-util/src/main/cpp/group_keys.cpp +++ b/libsession-util/src/main/cpp/group_keys.cpp @@ -295,4 +295,12 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_currentGeneration(J std::lock_guard lock{util::util_mutex_}; auto ptr = ptrToKeys(env, thiz); return ptr->current_generation(); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_GroupKeysConfig_admin(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto ptr = ptrToKeys(env, thiz); + return ptr->admin(); } \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index ac6f255117..46b7b99ae7 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -500,6 +500,7 @@ class GroupKeysConfig private constructor(pointer: Long): ConfigSig(pointer), Mu external override fun subAccountSign(message: ByteArray, signingValue: ByteArray): SwarmAuth external override fun currentGeneration(): Int + external fun admin(): Boolean data class SwarmAuth( val subAccount: String, diff --git a/libsession/src/main/java/org/session/libsession/messaging/groups/RemoveGroupMemberHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/groups/RemoveGroupMemberHandler.kt index 4fe0bfe22c..74b6b62779 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/groups/RemoveGroupMemberHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/groups/RemoveGroupMemberHandler.kt @@ -143,7 +143,7 @@ class RemoveGroupMemberHandler @Inject constructor( pendingRemovals to (calls as List) } - if (batchCalls.isEmpty()) { + if (pendingRemovals.isEmpty() || batchCalls.isEmpty()) { return } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt index ae3135167e..f49560b58e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt @@ -18,10 +18,12 @@ import org.session.libsession.messaging.jobs.MessageReceiveParameters import org.session.libsession.messaging.messages.Destination import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeClock import org.session.libsession.snode.model.RetrieveMessageResponse import org.session.libsession.snode.utilities.await 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.Log @@ -38,6 +40,7 @@ class ClosedGroupPoller( private val groupManagerV2: GroupManagerV2, private val storage: StorageProtocol, private val lokiApiDatabase: LokiAPIDatabaseProtocol, + private val clock: SnodeClock, ) { companion object { private const val POLL_INTERVAL = 3_000L @@ -112,9 +115,7 @@ class ClosedGroupPoller( } } - val adminKey = requireNotNull(configFactoryProtocol.withUserConfigs { - it.userGroups.getClosedGroup(closedGroupSessionId.hexString) - }) { + val adminKey = requireNotNull(configFactoryProtocol.getClosedGroup(closedGroupSessionId)) { "Group doesn't exist" }.adminKey @@ -135,7 +136,7 @@ class ClosedGroupPoller( maxSize = null, ), RetrieveMessageResponse::class.java - ) + ).messages.filterNotNull() } if (configHashesToExtends.isNotEmpty() && adminKey != null) { @@ -146,7 +147,7 @@ class ClosedGroupPoller( SnodeAPI.buildAuthenticatedAlterTtlBatchRequest( messageHashes = configHashesToExtends.toList(), auth = groupAuth, - newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, + newExpiry = clock.currentTimeMills() + 14.days.inWholeMilliseconds, extend = true ), ) @@ -191,7 +192,7 @@ class ClosedGroupPoller( maxSize = null, ), responseType = RetrieveMessageResponse::class.java - ) + ).messages.filterNotNull() } } @@ -238,23 +239,27 @@ class ClosedGroupPoller( } private fun RetrieveMessageResponse.Message.toConfigMessage(): ConfigMessage { - return ConfigMessage(hash, data, timestamp) + return ConfigMessage(hash, data, timestamp ?: clock.currentTimeMills()) } - private fun saveLastMessageHash(snode: Snode, body: RetrieveMessageResponse, namespace: Int) { - if (body.messages.isNotEmpty()) { + private fun saveLastMessageHash( + snode: Snode, + messages: List, + namespace: Int + ) { + if (messages.isNotEmpty()) { lokiApiDatabase.setLastMessageHashValue( snode = snode, publicKey = closedGroupSessionId.hexString, - newValue = body.messages.last().hash, + newValue = messages.last().hash, namespace = namespace ) } } - private suspend fun handleRevoked(body: RetrieveMessageResponse) { - body.messages.forEach { msg -> - val decoded = configFactoryProtocol.maybeDecryptForUser( + private suspend fun handleRevoked(messages: List) { + messages.forEach { msg -> + val decoded = configFactoryProtocol.decryptForUser( msg.data, Sodium.KICKED_DOMAIN, closedGroupSessionId, @@ -284,26 +289,26 @@ class ClosedGroupPoller( } private fun handleGroupConfigMessages( - keysResponse: RetrieveMessageResponse, - infoResponse: RetrieveMessageResponse, - membersResponse: RetrieveMessageResponse + keysResponse: List, + infoResponse: List, + membersResponse: List ) { - if (keysResponse.messages.isEmpty() && infoResponse.messages.isEmpty() && membersResponse.messages.isEmpty()) { + if (keysResponse.isEmpty() && infoResponse.isEmpty() && membersResponse.isEmpty()) { return } Log.d( TAG, "Handling group config messages(" + - "info = ${infoResponse.messages.size}, " + - "keys = ${keysResponse.messages.size}, " + - "members = ${membersResponse.messages.size})" + "info = ${infoResponse.size}, " + + "keys = ${keysResponse.size}, " + + "members = ${membersResponse.size})" ) configFactoryProtocol.mergeGroupConfigMessages( groupId = closedGroupSessionId, - keys = keysResponse.messages.map { it.toConfigMessage() }, - info = infoResponse.messages.map { it.toConfigMessage() }, - members = membersResponse.messages.map { it.toConfigMessage() }, + keys = keysResponse.map { it.toConfigMessage() }, + info = infoResponse.map { it.toConfigMessage() }, + members = membersResponse.map { it.toConfigMessage() }, ) } @@ -314,6 +319,7 @@ class ClosedGroupPoller( snode = snode, publicKey = closedGroupSessionId.hexString, decrypt = it.groupKeys::decrypt, + namespace = Namespace.CLOSED_GROUP_MESSAGES(), ) } @@ -331,7 +337,7 @@ class ClosedGroupPoller( } if (messages.isNotEmpty()) { - Log.d(TAG, "namespace for messages rx count: ${messages.size}") + Log.d(TAG, "Received and handled ${messages.size} group messages") } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index d3bcaad79f..bee4469cc9 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -48,7 +48,6 @@ import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.prettifiedDescription import org.session.libsignal.utilities.retryIfNeeded import org.session.libsignal.utilities.retryWithUniformInterval -import java.util.Date import java.util.Locale import kotlin.collections.component1 import kotlin.collections.component2 @@ -989,7 +988,7 @@ object SnodeAPI { } else Pair(MessageWrapper.unwrap(data), rawMessageAsJSON["hash"] as? String) } catch (e: Exception) { - Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") + Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.", e) null } } else { diff --git a/libsession/src/main/java/org/session/libsession/snode/model/MessageResponses.kt b/libsession/src/main/java/org/session/libsession/snode/model/MessageResponses.kt index 0e3cd270cf..b9173e1462 100644 --- a/libsession/src/main/java/org/session/libsession/snode/model/MessageResponses.kt +++ b/libsession/src/main/java/org/session/libsession/snode/model/MessageResponses.kt @@ -1,7 +1,11 @@ package org.session.libsession.snode.model +import android.util.Base64 import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.util.StdConverter data class StoreMessageResponse @JsonCreator constructor( @JsonProperty("hash") val hash: String, @@ -9,12 +13,29 @@ data class StoreMessageResponse @JsonCreator constructor( ) class RetrieveMessageResponse @JsonCreator constructor( - @JsonProperty("messages") val messages: List, + @JsonProperty("messages") + // Apply converter to the element so that if one of the message fails to deserialize, it will + // be a null value instead of failing the whole list. + @JsonDeserialize(contentConverter = RetrieveMessageConverter::class) + val messages: List, ) { - 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, + class Message( + val hash: String, + val timestamp: Long?, + val data: ByteArray, ) +} + +internal class RetrieveMessageConverter : StdConverter() { + override fun convert(value: JsonNode?): RetrieveMessageResponse.Message? { + value ?: return null + + val hash = value.get("hash")?.asText()?.takeIf { it.isNotEmpty() } ?: return null + val timestamp = value.get("t")?.asLong()?.takeIf { it > 0 } + val data = runCatching { + Base64.decode(value.get("data")?.asText().orEmpty(), Base64.DEFAULT) + }.getOrNull() ?: return null + + return RetrieveMessageResponse.Message(hash, timestamp, data) + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt index 33dcc5cb59..ff4cf45f53 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt @@ -3,7 +3,6 @@ package org.session.libsession.utilities import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withTimeoutOrNull import network.loki.messenger.libsession_util.MutableConfig @@ -48,9 +47,9 @@ interface ConfigFactoryProtocol { fun getGroupAuth(groupId: AccountId): SwarmAuth? fun removeGroup(groupId: AccountId) - fun maybeDecryptForUser(encoded: ByteArray, - domain: String, - closedGroupSessionId: AccountId): ByteArray? + fun decryptForUser(encoded: ByteArray, + domain: String, + closedGroupSessionId: AccountId): ByteArray? fun mergeGroupConfigMessages( groupId: AccountId,