Skip to content

Commit 4f057d6

Browse files
feat: create group conversation #WPB-18547
* Add Request and Response data classes for creating a Conversation * Add HTTP requests for claiming key packages, public keys and creating the group conversation * Add CoreCrypto calls for commiting pending proposals, update keying material and creating a conversation * Add createGroupConversation method on WireApplicationManager * Add sample for creating a conversation * Adjust and add new tests
1 parent 94490f8 commit 4f057d6

23 files changed

+946
-12
lines changed

lib/src/main/kotlin/com/wire/integrations/jvm/client/BackendClient.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ import com.wire.integrations.jvm.model.http.FeaturesResponse
2727
import com.wire.integrations.jvm.model.http.MlsPublicKeys
2828
import com.wire.integrations.jvm.model.http.client.RegisterClientRequest
2929
import com.wire.integrations.jvm.model.http.client.RegisterClientResponse
30+
import com.wire.integrations.jvm.model.http.conversation.ClaimedKeyPackageList
3031
import com.wire.integrations.jvm.model.http.conversation.ConversationResponse
32+
import com.wire.integrations.jvm.model.http.conversation.CreateConversationRequest
33+
import com.wire.integrations.jvm.model.http.conversation.MlsPublicKeysResponse
3134
import com.wire.integrations.jvm.model.http.user.UserResponse
3235
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
36+
import java.util.UUID
3337

3438
interface BackendClient {
3539
suspend fun connectWebSocket(handleFrames: suspend (DefaultClientWebSocketSession) -> Unit)
@@ -54,6 +58,14 @@ interface BackendClient {
5458
mlsKeyPackages: List<ByteArray>
5559
)
5660

61+
suspend fun claimKeyPackages(
62+
userDomain: String,
63+
userId: UUID,
64+
cipherSuite: String
65+
): ClaimedKeyPackageList
66+
67+
suspend fun getPublicKeys(): MlsPublicKeysResponse
68+
5769
suspend fun uploadCommitBundle(commitBundle: ByteArray)
5870

5971
suspend fun sendMessage(mlsMessage: ByteArray)
@@ -76,6 +88,10 @@ interface BackendClient {
7688
assetUploadData: AssetUploadData
7789
): AssetUploadResponse
7890

91+
suspend fun createGroupConversation(
92+
createConversationRequest: CreateConversationRequest
93+
): ConversationResponse
94+
7995
companion object {
8096
const val API_VERSION = "v9"
8197
}

lib/src/main/kotlin/com/wire/integrations/jvm/client/BackendClientDemo.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ import com.wire.integrations.jvm.model.http.MlsKeyPackageRequest
3232
import com.wire.integrations.jvm.model.http.MlsPublicKeys
3333
import com.wire.integrations.jvm.model.http.client.RegisterClientRequest
3434
import com.wire.integrations.jvm.model.http.client.RegisterClientResponse
35+
import com.wire.integrations.jvm.model.http.conversation.ClaimedKeyPackageList
3536
import com.wire.integrations.jvm.model.http.conversation.ConversationResponse
37+
import com.wire.integrations.jvm.model.http.conversation.CreateConversationRequest
38+
import com.wire.integrations.jvm.model.http.conversation.MlsPublicKeysResponse
3639
import com.wire.integrations.jvm.model.http.user.UserResponse
3740
import com.wire.integrations.jvm.persistence.AppStorage
3841
import com.wire.integrations.jvm.utils.Mls
@@ -45,6 +48,7 @@ import io.ktor.client.plugins.websocket.wss
4548
import io.ktor.client.request.accept
4649
import io.ktor.client.request.get
4750
import io.ktor.client.request.headers
51+
import io.ktor.client.request.parameter
4852
import io.ktor.client.request.post
4953
import io.ktor.client.request.prepareGet
5054
import io.ktor.client.request.put
@@ -228,6 +232,33 @@ internal class BackendClientDemo(
228232
logger.info("Updated client with mls key packages for client: $appClientId")
229233
}
230234

235+
override suspend fun claimKeyPackages(
236+
userDomain: String,
237+
userId: UUID,
238+
cipherSuite: String
239+
): ClaimedKeyPackageList {
240+
val token = loginUser()
241+
return httpClient.post("$API_VERSION/mls/key-packages/claim/$userDomain/$userId") {
242+
headers {
243+
append(HttpHeaders.Authorization, "Bearer $token")
244+
}
245+
parameter("ciphersuite", cipherSuite)
246+
contentType(ContentType.Application.Json)
247+
accept(ContentType.Application.Json)
248+
}.body<ClaimedKeyPackageList>()
249+
}
250+
251+
override suspend fun getPublicKeys(): MlsPublicKeysResponse {
252+
val token = loginUser()
253+
return httpClient.get("$API_VERSION/mls/public-keys") {
254+
headers {
255+
append(HttpHeaders.Authorization, "Bearer $token")
256+
}
257+
contentType(ContentType.Application.Json)
258+
accept(ContentType.Application.Json)
259+
}.body<MlsPublicKeysResponse>()
260+
}
261+
231262
override suspend fun uploadCommitBundle(commitBundle: ByteArray) {
232263
val token = loginUser()
233264
httpClient.post("/$API_VERSION/mls/commit-bundles") {
@@ -331,6 +362,20 @@ internal class BackendClientDemo(
331362
}.body<AssetUploadResponse>()
332363
}
333364

365+
override suspend fun createGroupConversation(
366+
createConversationRequest: CreateConversationRequest
367+
): ConversationResponse {
368+
val token = loginUser()
369+
return httpClient.post("/$API_VERSION/conversations") {
370+
headers {
371+
append(HttpHeaders.Authorization, "Bearer $token")
372+
}
373+
setBody(createConversationRequest)
374+
contentType(ContentType.Application.Json)
375+
accept(ContentType.Application.Json)
376+
}.body<ConversationResponse>()
377+
}
378+
334379
internal class AssetBody internal constructor(
335380
private val assetContent: ByteArray,
336381
assetSize: Long,

lib/src/main/kotlin/com/wire/integrations/jvm/client/BackendClientImpl.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ import com.wire.integrations.jvm.model.http.FeaturesResponse
2929
import com.wire.integrations.jvm.model.http.MlsPublicKeys
3030
import com.wire.integrations.jvm.model.http.client.RegisterClientRequest
3131
import com.wire.integrations.jvm.model.http.client.RegisterClientResponse
32+
import com.wire.integrations.jvm.model.http.conversation.ClaimedKeyPackageList
3233
import com.wire.integrations.jvm.model.http.conversation.ConversationResponse
34+
import com.wire.integrations.jvm.model.http.conversation.CreateConversationRequest
35+
import com.wire.integrations.jvm.model.http.conversation.MlsPublicKeysResponse
3336
import com.wire.integrations.jvm.model.http.user.UserResponse
3437
import io.ktor.client.HttpClient
3538
import io.ktor.client.call.body
@@ -39,6 +42,7 @@ import io.ktor.client.request.get
3942
import io.ktor.client.request.header
4043
import io.ktor.client.request.post
4144
import io.ktor.http.HttpHeaders
45+
import java.util.UUID
4246
import org.slf4j.LoggerFactory
4347

4448
/**
@@ -102,6 +106,18 @@ internal class BackendClientImpl(private val httpClient: HttpClient) : BackendCl
102106
TODO("Not yet implemented")
103107
}
104108

109+
override suspend fun claimKeyPackages(
110+
userDomain: String,
111+
userId: UUID,
112+
cipherSuite: String
113+
): ClaimedKeyPackageList {
114+
TODO("Not yet implemented")
115+
}
116+
117+
override suspend fun getPublicKeys(): MlsPublicKeysResponse {
118+
TODO("Not yet implemented")
119+
}
120+
105121
override suspend fun uploadCommitBundle(commitBundle: ByteArray) {
106122
TODO("Not yet implemented")
107123
}
@@ -137,4 +153,10 @@ internal class BackendClientImpl(private val httpClient: HttpClient) : BackendCl
137153
): AssetUploadResponse {
138154
TODO("Not yet implemented")
139155
}
156+
157+
override suspend fun createGroupConversation(
158+
createConversationRequest: CreateConversationRequest
159+
): ConversationResponse {
160+
TODO("Not yet implemented")
161+
}
140162
}

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ 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
78
import com.wire.crypto.GroupInfo
89
import com.wire.crypto.MLSGroupId
910
import com.wire.crypto.MLSKeyPackage
@@ -146,15 +147,37 @@ internal class CoreCryptoClient private constructor(
146147
return coreCrypto.transaction { it.joinByExternalCommit(groupInfo).id }
147148
}
148149

149-
override suspend fun createConversation(groupId: MLSGroupId) {
150+
override suspend fun createConversation(
151+
groupId: MLSGroupId,
152+
externalSenders: ByteArray
153+
) {
150154
return coreCrypto.transaction {
151155
it.createConversation(
152156
id = groupId,
153-
ciphersuite = ciphersuite
157+
ciphersuite = ciphersuite,
158+
externalSenders = listOf(
159+
ExternalSenderKey(
160+
value = com.wire.crypto.uniffi.ExternalSenderKey(
161+
bytes = externalSenders
162+
)
163+
)
164+
)
154165
)
155166
}
156167
}
157168

169+
override suspend fun commitPendingProposals(mlsGroupId: MLSGroupId) {
170+
coreCrypto.transaction {
171+
it.commitPendingProposals(mlsGroupId)
172+
}
173+
}
174+
175+
override suspend fun updateKeyingMaterial(mlsGroupId: MLSGroupId) {
176+
coreCrypto.transaction {
177+
it.updateKeyingMaterial(mlsGroupId)
178+
}
179+
}
180+
158181
override suspend fun addMemberToMlsConversation(
159182
mlsGroupId: MLSGroupId,
160183
keyPackages: List<MLSKeyPackage>
@@ -215,7 +238,7 @@ internal class CoreCryptoClient private constructor(
215238
)
216239
}
217240

218-
private fun getMlsCipherSuiteName(code: Int): Ciphersuite =
241+
fun getMlsCipherSuiteName(code: Int): Ciphersuite =
219242
when (code) {
220243
DEFAULT_CIPHERSUITE_IDENTIFIER -> Ciphersuite.DEFAULT
221244
2 -> Ciphersuite.MLS_128_DHKEMP256_AES128GCM_SHA256_P256

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,14 @@ internal interface CryptoClient : AutoCloseable {
7272
/**
7373
* Create an MLS conversation, adding the client as the first member.
7474
*/
75-
suspend fun createConversation(groupId: MLSGroupId)
75+
suspend fun createConversation(
76+
groupId: MLSGroupId,
77+
externalSenders: ByteArray
78+
)
79+
80+
suspend fun commitPendingProposals(mlsGroupId: MLSGroupId)
81+
82+
suspend fun updateKeyingMaterial(mlsGroupId: MLSGroupId)
7683

7784
/**
7885
* Alternative way to add a member to an MLS conversation.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.model.http.conversation
18+
19+
import kotlinx.serialization.SerialName
20+
import kotlinx.serialization.Serializable
21+
22+
@Serializable
23+
enum class ConversationAccess {
24+
@SerialName("private")
25+
PRIVATE,
26+
27+
@SerialName("code")
28+
CODE,
29+
30+
@SerialName("invite")
31+
INVITE,
32+
33+
@SerialName("self_invite")
34+
SELF_INVITE,
35+
36+
@SerialName("link")
37+
LINK;
38+
39+
override fun toString(): String {
40+
return this.name.lowercase()
41+
}
42+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.model.http.conversation
18+
19+
import kotlinx.serialization.SerialName
20+
import kotlinx.serialization.Serializable
21+
22+
@Serializable
23+
enum class ConversationAccessRole {
24+
@SerialName("team_member")
25+
TEAM_MEMBER,
26+
27+
@SerialName("non_team_member")
28+
NON_TEAM_MEMBER,
29+
30+
@SerialName("guest")
31+
GUEST,
32+
33+
@SerialName("service")
34+
SERVICE,
35+
36+
@SerialName("partner")
37+
EXTERNAL;
38+
39+
override fun toString(): String {
40+
return this.name.lowercase()
41+
}
42+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.model.http.conversation
18+
19+
import kotlinx.serialization.SerialName
20+
import kotlinx.serialization.Serializable
21+
22+
@Serializable
23+
enum class ConversationProtocol {
24+
@SerialName("proteus")
25+
PROTEUS,
26+
27+
@SerialName("mls")
28+
MLS,
29+
30+
@SerialName("mixed")
31+
MIXED;
32+
33+
override fun toString(): String {
34+
return this.name.lowercase()
35+
}
36+
}

lib/src/main/kotlin/com/wire/integrations/jvm/model/http/conversation/ConversationResponse.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ data class ConversationResponse(
3838
@SerialName("members")
3939
val members: ConversationMembers,
4040
@SerialName("type")
41-
val type: Type
41+
val type: Type,
42+
@SerialName("public_keys")
43+
val publicKeys: MlsPublicKeysResponse? = null
4244
) {
4345
@Serializable
4446
enum class Type {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.model.http.conversation
18+
19+
import kotlinx.serialization.SerialName
20+
import kotlinx.serialization.Serializable
21+
22+
@Serializable
23+
data class ConversationTeamInfo(
24+
@Deprecated("Not parsed any more")
25+
@SerialName("managed") val managed: Boolean,
26+
@SerialName("teamid") val teamId: String
27+
)

0 commit comments

Comments
 (0)