Skip to content

Commit 306bc56

Browse files
feat: create group conversation #WPB-18547
* Move create group conversation logic from WireApplicationManager to its own Service class
1 parent 4f057d6 commit 306bc56

File tree

4 files changed

+160
-118
lines changed

4 files changed

+160
-118
lines changed

lib/src/main/kotlin/com/wire/integrations/jvm/config/Modules.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import com.wire.integrations.jvm.service.EventsRouter
4040
import com.wire.integrations.jvm.service.MlsFallbackStrategy
4141
import com.wire.integrations.jvm.service.WireApplicationManager
4242
import com.wire.integrations.jvm.service.WireTeamEventsListener
43+
import com.wire.integrations.jvm.service.conversation.CreateGroupConversationService
4344
import com.wire.integrations.jvm.utils.KtxSerializer
4445
import com.wire.integrations.jvm.utils.mls
4546
import com.wire.integrations.jvm.utils.obfuscateClientId
@@ -91,7 +92,12 @@ val sdkModule =
9192
}
9293
} onClose { it?.close() }
9394
single { WireTeamEventsListener(get(), get()) }
94-
single { WireApplicationManager(get(), get(), get(), get(), get()) }
95+
96+
// Services
97+
single { CreateGroupConversationService(get(), get()) }
98+
99+
// Manager
100+
single { WireApplicationManager(get(), get(), get(), get(), get(), get()) }
95101
}
96102

97103
@OptIn(ExperimentalLogbookKtorApi::class)

lib/src/main/kotlin/com/wire/integrations/jvm/crypto/CoreCryptoClient.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import com.wire.crypto.Ciphersuite
44
import com.wire.crypto.Ciphersuites
55
import com.wire.crypto.CoreCrypto
66
import com.wire.crypto.DatabaseKey
7-
import com.wire.crypto.ExternalSenderKey
87
import com.wire.crypto.GroupInfo
98
import com.wire.crypto.MLSGroupId
109
import com.wire.crypto.MLSKeyPackage
1110
import com.wire.crypto.MlsTransport
1211
import com.wire.crypto.Welcome
1312
import com.wire.crypto.toClientId
13+
import com.wire.crypto.toExternalSenderKey
1414
import com.wire.integrations.jvm.config.IsolatedKoinContext
1515
import com.wire.integrations.jvm.crypto.CryptoClient.Companion.DEFAULT_KEYPACKAGE_COUNT
1616
import com.wire.integrations.jvm.exception.WireException
@@ -156,11 +156,7 @@ internal class CoreCryptoClient private constructor(
156156
id = groupId,
157157
ciphersuite = ciphersuite,
158158
externalSenders = listOf(
159-
ExternalSenderKey(
160-
value = com.wire.crypto.uniffi.ExternalSenderKey(
161-
bytes = externalSenders
162-
)
163-
)
159+
externalSenders.toExternalSenderKey()
164160
)
165161
)
166162
}

lib/src/main/kotlin/com/wire/integrations/jvm/service/WireApplicationManager.kt

Lines changed: 6 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@
1515

1616
package com.wire.integrations.jvm.service
1717

18-
import com.wire.crypto.MLSGroupId
19-
import com.wire.crypto.MLSKeyPackage
20-
import com.wire.crypto.toGroupId
2118
import com.wire.integrations.jvm.client.BackendClient
22-
import com.wire.integrations.jvm.crypto.CoreCryptoClient
2319
import com.wire.integrations.jvm.crypto.CryptoClient
2420
import com.wire.integrations.jvm.exception.WireException
2521
import com.wire.integrations.jvm.model.AssetResource
@@ -34,27 +30,20 @@ import com.wire.integrations.jvm.model.asset.AssetRetention
3430
import com.wire.integrations.jvm.model.asset.AssetUploadData
3531
import com.wire.integrations.jvm.model.http.ApiVersionResponse
3632
import com.wire.integrations.jvm.model.http.AppDataResponse
37-
import com.wire.integrations.jvm.model.http.conversation.ConversationTeamInfo
38-
import com.wire.integrations.jvm.model.http.conversation.CreateConversationRequest
39-
import com.wire.integrations.jvm.model.http.conversation.KeyPackage
40-
import com.wire.integrations.jvm.model.http.conversation.getRemovalKey
4133
import com.wire.integrations.jvm.model.http.user.UserResponse
4234
import com.wire.integrations.jvm.model.protobuf.ProtobufSerializer
4335
import com.wire.integrations.jvm.persistence.ConversationStorage
4436
import com.wire.integrations.jvm.persistence.TeamStorage
37+
import com.wire.integrations.jvm.service.conversation.CreateGroupConversationService
4538
import com.wire.integrations.jvm.utils.AESDecrypt
4639
import com.wire.integrations.jvm.utils.AESEncrypt
4740
import com.wire.integrations.jvm.utils.MAX_DATA_SIZE
48-
import com.wire.integrations.jvm.utils.toHexString
49-
import io.ktor.util.decodeBase64Bytes
5041
import java.io.ByteArrayInputStream
51-
import java.util.Base64
5242
import java.util.UUID
5343
import javax.imageio.ImageIO
5444
import kotlinx.coroutines.Dispatchers
5545
import kotlinx.coroutines.runBlocking
5646
import kotlinx.coroutines.withContext
57-
import org.slf4j.LoggerFactory
5847

5948
/**
6049
* Allows fetching common data and interacting with each Team instance invited to the Application.
@@ -66,10 +55,9 @@ class WireApplicationManager internal constructor(
6655
private val conversationStorage: ConversationStorage,
6756
private val backendClient: BackendClient,
6857
private val cryptoClient: CryptoClient,
69-
private val mlsFallbackStrategy: MlsFallbackStrategy
58+
private val mlsFallbackStrategy: MlsFallbackStrategy,
59+
private val createGroupConversationService: CreateGroupConversationService
7060
) {
71-
private val logger = LoggerFactory.getLogger("WireApplicationManager")
72-
7361
fun getStoredTeams(): List<TeamId> = teamStorage.getAll()
7462

7563
fun getStoredConversations(): List<ConversationData> = conversationStorage.getAll()
@@ -375,103 +363,10 @@ class WireApplicationManager internal constructor(
375363
name: String,
376364
teamId: TeamId,
377365
userIds: List<QualifiedId>
378-
): QualifiedId {
379-
val conversationRequest = CreateConversationRequest(
366+
): QualifiedId =
367+
createGroupConversationService.create(
380368
name = name,
381-
conversationTeamInfo = ConversationTeamInfo(
382-
managed = false,
383-
teamId = teamId.value.toString()
384-
)
385-
)
386-
387-
val conversationResponse = backendClient.createGroupConversation(
388-
createConversationRequest = conversationRequest
389-
)
390-
391-
val mlsGroupId = Base64
392-
.getDecoder()
393-
.decode(conversationResponse.groupId)
394-
.toGroupId()
395-
396-
val cipherSuiteCode = backendClient
397-
.getApplicationFeatures()
398-
.mlsFeatureResponse
399-
.mlsFeatureConfigResponse
400-
.defaultCipherSuite
401-
402-
val cipherSuite = CoreCryptoClient.getMlsCipherSuiteName(code = cipherSuiteCode)
403-
404-
val publicKeys = (conversationResponse.publicKeys ?: backendClient.getPublicKeys())
405-
.getRemovalKey(cipherSuite = cipherSuite)
406-
407-
createMlsGroupConversation(
408-
publicKeys = publicKeys,
409-
cipherSuiteCode = cipherSuiteCode,
410-
mlsGroupId = mlsGroupId,
369+
teamId = teamId,
411370
userIds = userIds
412371
)
413-
414-
val conversationId = conversationResponse.id
415-
logger.info("Conversation created with ID: $conversationId")
416-
return conversationId
417-
}
418-
419-
private suspend fun createMlsGroupConversation(
420-
publicKeys: ByteArray?,
421-
cipherSuiteCode: Int,
422-
mlsGroupId: MLSGroupId,
423-
userIds: List<QualifiedId>
424-
) {
425-
publicKeys?.let { externalSenders ->
426-
cryptoClient.createConversation(
427-
groupId = mlsGroupId,
428-
externalSenders = externalSenders
429-
)
430-
431-
cryptoClient.commitPendingProposals(mlsGroupId)
432-
433-
val claimedKeyPackages: List<ByteArray> = claimKeyPackages(
434-
userIds = userIds,
435-
cipherSuiteCode = cipherSuiteCode
436-
)
437-
438-
if (claimedKeyPackages.isEmpty()) {
439-
cryptoClient.updateKeyingMaterial(mlsGroupId)
440-
} else {
441-
cryptoClient.addMemberToMlsConversation(
442-
mlsGroupId = mlsGroupId,
443-
keyPackages = claimedKeyPackages.map { keyPackage ->
444-
MLSKeyPackage(com.wire.crypto.uniffi.KeyPackage(keyPackage))
445-
}
446-
)
447-
}
448-
}
449-
}
450-
451-
@Suppress("TooGenericExceptionCaught")
452-
private suspend fun claimKeyPackages(
453-
userIds: List<QualifiedId>,
454-
cipherSuiteCode: Int
455-
): List<ByteArray> {
456-
val claimedKeyPackages = mutableListOf<KeyPackage>()
457-
userIds.forEach { user ->
458-
try {
459-
val result = backendClient.claimKeyPackages(
460-
userDomain = user.domain,
461-
userId = user.id,
462-
cipherSuite = cipherSuiteCode.toHexString()
463-
)
464-
465-
if (result.keyPackages.isNotEmpty()) {
466-
claimedKeyPackages.addAll(result.keyPackages)
467-
}
468-
} catch (exception: Exception) {
469-
// Ignoring when claiming key packages fails for a user
470-
// as for now there is no retry
471-
logger.info("Error when claiming key packages: $exception")
472-
}
473-
}
474-
475-
return claimedKeyPackages.map { it.keyPackage.decodeBase64Bytes() }
476-
}
477372
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2025 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see http://www.gnu.org/licenses/.
15+
*/
16+
17+
package com.wire.integrations.jvm.service.conversation
18+
19+
import com.wire.crypto.MLSGroupId
20+
import com.wire.crypto.toGroupId
21+
import com.wire.crypto.toMLSKeyPackage
22+
import com.wire.integrations.jvm.client.BackendClient
23+
import com.wire.integrations.jvm.crypto.CoreCryptoClient
24+
import com.wire.integrations.jvm.crypto.CryptoClient
25+
import com.wire.integrations.jvm.model.QualifiedId
26+
import com.wire.integrations.jvm.model.TeamId
27+
import com.wire.integrations.jvm.model.http.conversation.ConversationTeamInfo
28+
import com.wire.integrations.jvm.model.http.conversation.CreateConversationRequest
29+
import com.wire.integrations.jvm.model.http.conversation.KeyPackage
30+
import com.wire.integrations.jvm.model.http.conversation.getRemovalKey
31+
import com.wire.integrations.jvm.utils.toHexString
32+
import io.ktor.util.decodeBase64Bytes
33+
import java.util.Base64
34+
import org.slf4j.LoggerFactory
35+
36+
internal class CreateGroupConversationService internal constructor(
37+
private val backendClient: BackendClient,
38+
private val cryptoClient: CryptoClient
39+
) {
40+
private val logger = LoggerFactory.getLogger(this::class.java)
41+
42+
suspend fun create(
43+
name: String,
44+
teamId: TeamId,
45+
userIds: List<QualifiedId>
46+
): QualifiedId {
47+
val conversationRequest = CreateConversationRequest(
48+
name = name,
49+
conversationTeamInfo = ConversationTeamInfo(
50+
managed = false,
51+
teamId = teamId.value.toString()
52+
)
53+
)
54+
55+
val conversationResponse = backendClient.createGroupConversation(
56+
createConversationRequest = conversationRequest
57+
)
58+
59+
val mlsGroupId = Base64
60+
.getDecoder()
61+
.decode(conversationResponse.groupId)
62+
.toGroupId()
63+
64+
val cipherSuiteCode = backendClient
65+
.getApplicationFeatures()
66+
.mlsFeatureResponse
67+
.mlsFeatureConfigResponse
68+
.defaultCipherSuite
69+
70+
val cipherSuite = CoreCryptoClient.getMlsCipherSuiteName(code = cipherSuiteCode)
71+
72+
val publicKeys = (conversationResponse.publicKeys ?: backendClient.getPublicKeys())
73+
.getRemovalKey(cipherSuite = cipherSuite)
74+
75+
createMlsGroupConversation(
76+
publicKeys = publicKeys,
77+
cipherSuiteCode = cipherSuiteCode,
78+
mlsGroupId = mlsGroupId,
79+
userIds = userIds
80+
)
81+
82+
val conversationId = conversationResponse.id
83+
logger.info("Conversation created with ID: $conversationId")
84+
return conversationId
85+
}
86+
87+
private suspend fun createMlsGroupConversation(
88+
publicKeys: ByteArray?,
89+
cipherSuiteCode: Int,
90+
mlsGroupId: MLSGroupId,
91+
userIds: List<QualifiedId>
92+
) {
93+
publicKeys?.let { externalSenders ->
94+
cryptoClient.createConversation(
95+
groupId = mlsGroupId,
96+
externalSenders = externalSenders
97+
)
98+
99+
cryptoClient.commitPendingProposals(mlsGroupId)
100+
101+
val claimedKeyPackages: List<ByteArray> = claimKeyPackages(
102+
userIds = userIds,
103+
cipherSuiteCode = cipherSuiteCode
104+
)
105+
106+
if (claimedKeyPackages.isEmpty()) {
107+
cryptoClient.updateKeyingMaterial(mlsGroupId)
108+
} else {
109+
cryptoClient.addMemberToMlsConversation(
110+
mlsGroupId = mlsGroupId,
111+
keyPackages = claimedKeyPackages.map { keyPackage ->
112+
keyPackage.toMLSKeyPackage()
113+
}
114+
)
115+
}
116+
}
117+
}
118+
119+
@Suppress("TooGenericExceptionCaught")
120+
private suspend fun claimKeyPackages(
121+
userIds: List<QualifiedId>,
122+
cipherSuiteCode: Int
123+
): List<ByteArray> {
124+
val claimedKeyPackages = mutableListOf<KeyPackage>()
125+
userIds.forEach { user ->
126+
try {
127+
val result = backendClient.claimKeyPackages(
128+
userDomain = user.domain,
129+
userId = user.id,
130+
cipherSuite = cipherSuiteCode.toHexString()
131+
)
132+
133+
if (result.keyPackages.isNotEmpty()) {
134+
claimedKeyPackages.addAll(result.keyPackages)
135+
}
136+
} catch (exception: Exception) {
137+
// Ignoring when claiming key packages fails for a user
138+
// as for now there is no retry
139+
logger.info("Error when claiming key packages: $exception")
140+
}
141+
}
142+
143+
return claimedKeyPackages.map { it.keyPackage.decodeBase64Bytes() }
144+
}
145+
}

0 commit comments

Comments
 (0)