diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index e77a8fa42..54dc4a794 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -19,6 +19,7 @@ import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.UserPic +import nl.komponents.kovenant.functional.bind import org.session.libsession.avatars.AvatarHelper import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.BlindedIdMapping @@ -1277,7 +1278,78 @@ open class Storage( override fun inviteClosedGroupMembers(groupSessionId: String, invitees: List) { // don't try to process invitee acceptance if we aren't admin if (configFactory.userGroups?.getClosedGroup(groupSessionId)?.hasAdminKey() != true) return - val infoConfig = configFactory + val adminKey = configFactory.userGroups?.getClosedGroup(groupSessionId)?.adminKey ?: return + val sessionId = SessionId.from(groupSessionId) + val membersConfig = configFactory.getGroupMemberConfig(sessionId) ?: return + val infoConfig = configFactory.getGroupInfoConfig(sessionId) ?: return + + val filteredMembers = invitees.filter { + membersConfig.get(it) == null + } + filteredMembers.forEach { memberSessionId -> + val member = membersConfig.getOrConstruct(memberSessionId).copy( + invitePending = true, + ) + membersConfig.set(member) + } + + val keysConfig = configFactory.getGroupKeysConfig( + sessionId, + info = infoConfig, + members = membersConfig, + free = false + ) ?: return + + keysConfig.rekey(infoConfig, membersConfig) + + val sentTimestamp = SnodeAPI.nowWithOffset + + val message = SnodeMessage( + groupSessionId, + Base64.encodeBytes(keysConfig.pendingConfig()!!), // should not be null from checking has pending + SnodeMessage.CONFIG_TTL, + sentTimestamp + ) + val authenticatedBatch = SnodeAPI.buildAuthenticatedStoreBatchInfo( + keysConfig.namespace(), + message, + adminKey + ) + + val response = SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode -> + SnodeAPI.getRawBatchResponse( + snode, + groupSessionId, + listOf(authenticatedBatch), + ) + } + + val destination = Destination.ClosedGroup(groupSessionId) + + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(destination) + + try { + response.get() + } catch (e: Exception) { + Log.e("ClosedGroup", "Failed to store new key", e) + infoConfig.free() + membersConfig.free() + keysConfig.free() + // toaster toast here + return + } + + configFactory.saveGroupConfigs(keysConfig, infoConfig, membersConfig) + + infoConfig.free() + membersConfig.free() + keysConfig.free() + + val newConfigSync = ConfigurationSyncJob(destination) + JobQueue.shared.add(newConfigSync) + + val job = InviteContactsJob(groupSessionId, filteredMembers.toTypedArray()) + JobQueue.shared.add(job) } override fun setServerCapabilities(server: String, capabilities: List) { 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 8e2fcb9a7..d81b7a59e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -221,6 +221,8 @@ class ConfigFactory( info?.free() members?.free() } + if (usedInfo !== info) usedInfo.free() + if (usedMembers !== members) usedMembers.free() keys } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt index 44fd3a580..3325d3e0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt @@ -48,8 +48,6 @@ import network.loki.messenger.R import network.loki.messenger.libsession_util.util.GroupMember import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.messaging.jobs.InviteContactsJob -import org.session.libsession.messaging.jobs.JobQueue import org.thoughtcrime.securesms.groups.ContactList import org.thoughtcrime.securesms.groups.destinations.EditClosedGroupInviteScreenDestination import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin @@ -98,7 +96,6 @@ fun EditClosedGroupInviteScreen( val state by viewModel.viewState.collectAsState() val viewState = state.viewState val currentMemberSessionIds = viewState.currentMembers.map { it.memberSessionId } - val eventSink = state.eventSink SelectContacts( viewState.allContacts @@ -156,15 +153,10 @@ class EditGroupViewModel @AssistedInject constructor( when (event) { is EditGroupEvent.InviteContacts -> { val sessionIds = event.contacts - val invite = InviteContactsJob( - groupSessionId, - sessionIds.contacts.map(Contact::sessionID).toTypedArray() - ) storage.inviteClosedGroupMembers( groupSessionId, sessionIds.contacts.map(Contact::sessionID) ) - JobQueue.shared.add(invite) Toast.makeText( event.context, "Inviting ${event.contacts.contacts.size}", @@ -210,9 +202,7 @@ class EditGroupInviteViewModel @AssistedInject constructor( EditGroupInviteState( EditGroupInviteViewState(closedGroupMembers, contacts) - ) { event -> - - } + ) } @AssistedFactory @@ -321,7 +311,6 @@ data class EditGroupState( data class EditGroupInviteState( val viewState: EditGroupInviteViewState, - val eventSink: (Unit) -> Unit ) data class MemberViewModel( diff --git a/libsession-util/src/main/cpp/group_keys.cpp b/libsession-util/src/main/cpp/group_keys.cpp index 7b32338bc..7c6b99db9 100644 --- a/libsession-util/src/main/cpp/group_keys.cpp +++ b/libsession-util/src/main/cpp/group_keys.cpp @@ -104,6 +104,8 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_needsDump(JNIEnv *e return keys->needs_dump(); } + + extern "C" JNIEXPORT jbyteArray JNICALL Java_network_loki_messenger_libsession_1util_GroupKeysConfig_pendingKey(JNIEnv *env, jobject thiz) { @@ -257,4 +259,18 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_subAccountSign(JNIE auto signing_value_ustring = util::ustring_from_bytes(env, signing_value); auto swarm_auth = ptr->swarm_subaccount_sign(message_ustring, signing_value_ustring, false); return util::deserialize_swarm_auth(env, swarm_auth); +} + +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_GroupKeysConfig_supplementFor(JNIEnv *env, + jobject thiz, + jstring user_session_id) { + std::lock_guard lock{util::util_mutex_}; + auto ptr = ptrToKeys(env, thiz); + auto string = env->GetStringUTFChars(user_session_id, nullptr); + auto supplement = ptr->key_supplement(string); + auto supplement_jbytearray = util::bytes_from_ustring(env, supplement); + env->ReleaseStringUTFChars(user_session_id, string); + return supplement_jbytearray; } \ 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 dad97251b..4378f2270 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 @@ -321,6 +321,7 @@ class GroupKeysConfig(pointer: Long): ConfigSig(pointer) { members: GroupMembersConfig) external fun needsRekey(): Boolean external fun pendingKey(): ByteArray? + external fun supplementFor(userSessionId: String): ByteArray external fun pendingConfig(): ByteArray? external fun currentHashes(): List external fun rekey(info: GroupInfoConfig, members: GroupMembersConfig): ByteArray diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt index 1be8e62c4..243d1c5ef 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt @@ -234,8 +234,8 @@ data class ConfigurationSyncJob(val destination: Destination) : Job { responseBody?.get("hash") as? String ?: run { Log.w( - TAG, - "No hash returned for the configuration in namespace ${config.namespace()}" + TAG, + "No hash returned for the configuration in namespace ${config.namespace()}" ) return@forEachIndexed } @@ -247,8 +247,8 @@ data class ConfigurationSyncJob(val destination: Destination) : Job { } Log.d( - TAG, - "Successfully removed the deleted hashes from ${config.javaClass.simpleName}" + TAG, + "Successfully removed the deleted hashes from ${config.javaClass.simpleName}" ) // dump and write config after successful if (config is ConfigBase && config.needsDump()) { // usually this will be true? )) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index c38b3f143..7ff5c4664 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -24,10 +24,13 @@ import kotlin.math.roundToLong class JobQueue : JobDelegate { private var hasResumedPendingJobs = false // Just for debugging private val jobTimestampMap = ConcurrentHashMap() + private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() private val openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher() private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val configDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob() private val queue = Channel(UNLIMITED) private val pendingJobIds = mutableSetOf() @@ -114,19 +117,23 @@ class JobQueue : JobDelegate { val txQueue = Channel(capacity = UNLIMITED) val mediaQueue = Channel(capacity = UNLIMITED) val openGroupQueue = Channel(capacity = UNLIMITED) + val configQueue = Channel(capacity = UNLIMITED) val receiveJob = processWithDispatcher(rxQueue, rxDispatcher, "rx", asynchronous = false) val txJob = processWithDispatcher(txQueue, txDispatcher, "tx") val mediaJob = processWithDispatcher(mediaQueue, rxMediaDispatcher, "media") val openGroupJob = processWithOpenGroupDispatcher(openGroupQueue, openGroupDispatcher, "openGroup") + val configJob = processWithDispatcher(configQueue, configDispatcher, "configDispatcher") while (isActive) { when (val job = queue.receive()) { + is InviteContactsJob, + is ConfigurationSyncJob -> { + configQueue.send(job) + } is NotifyPNServerJob, is AttachmentUploadJob, - is MessageSendJob, - is ConfigurationSyncJob, - is InviteContactsJob -> { + is MessageSendJob -> { txQueue.send(job) } is RetrieveProfileAvatarJob, @@ -158,6 +165,7 @@ class JobQueue : JobDelegate { txJob.cancel() mediaJob.cancel() openGroupJob.cancel() + configJob.cancel() } }