Skip to content

Commit c91ca73

Browse files
committed
Merge branch 'main' into st/group-preference-actions
2 parents 12c7a74 + 6f8209f commit c91ca73

File tree

5 files changed

+137
-55
lines changed

5 files changed

+137
-55
lines changed

library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt

+9-13
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package org.xmtp.android.library
33
import androidx.test.ext.junit.runners.AndroidJUnit4
44
import androidx.test.platform.app.InstrumentationRegistry
55
import org.junit.Assert.assertEquals
6-
import org.junit.Assert.assertNull
76
import org.junit.Assert.fail
87
import org.junit.Ignore
98
import org.junit.Test
@@ -91,10 +90,7 @@ class ClientTest {
9190
)
9291
val client =
9392
Client().create(account = fakeWallet, options = options)
94-
assertEquals(
95-
client.address.lowercase(),
96-
client.libXMTPClient?.accountAddress()?.lowercase()
97-
)
93+
assert(client.canMessageV3(listOf(client.address)))
9894

9995
val bundle = client.privateKeyBundle
10096
val clientFromV1Bundle = Client().buildFromBundle(bundle, account = fakeWallet, options = options)
@@ -103,9 +99,12 @@ class ClientTest {
10399
client.privateKeyBundleV1.identityKey,
104100
clientFromV1Bundle.privateKeyBundleV1.identityKey,
105101
)
102+
103+
assert(clientFromV1Bundle.canMessageV3(listOf(client.address)))
104+
106105
assertEquals(
107-
client.libXMTPClient?.accountAddress(),
108-
clientFromV1Bundle.libXMTPClient?.accountAddress()
106+
client.address,
107+
clientFromV1Bundle.address
109108
)
110109
}
111110

@@ -122,8 +121,7 @@ class ClientTest {
122121
appContext = context
123122
)
124123
)
125-
val v3Client = client.libXMTPClient
126-
assertEquals(client.address.lowercase(), v3Client?.accountAddress()?.lowercase())
124+
assert(client.canMessageV3(listOf(client.address)))
127125
}
128126

129127
@Test
@@ -139,16 +137,14 @@ class ClientTest {
139137
appContext = context
140138
)
141139
)
142-
val v3Client = client.libXMTPClient
143-
assertEquals(client.address.lowercase(), v3Client?.accountAddress()?.lowercase())
140+
assert(client.canMessageV3(listOf(client.address)))
144141
}
145142

146143
@Test
147144
fun testDoesNotCreateAV3Client() {
148145
val fakeWallet = PrivateKeyBuilder()
149146
val client = Client().create(account = fakeWallet)
150-
val v3Client = client.libXMTPClient
151-
assertNull(v3Client)
147+
assert(!client.canMessageV3(listOf(client.address)))
152148
}
153149

154150
@Test

library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt

+66-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.xmtp.android.library.codecs.ReactionSchema
1717
import org.xmtp.android.library.messages.PrivateKey
1818
import org.xmtp.android.library.messages.PrivateKeyBuilder
1919
import org.xmtp.android.library.messages.walletAddress
20+
import uniffi.xmtpv3.GroupPermissions
2021

2122
@RunWith(AndroidJUnit4::class)
2223
class GroupTest {
@@ -57,9 +58,63 @@ class GroupTest {
5758
}
5859

5960
@Test
60-
fun testCanCreateAGroup() {
61-
val group = boClient.conversations.newGroup(listOf(alix.walletAddress))
62-
assert(group.id.isNotEmpty())
61+
fun testCanCreateAGroupWithDefaultPermissions() {
62+
val boGroup = boClient.conversations.newGroup(listOf(alix.walletAddress))
63+
runBlocking { alixClient.conversations.syncGroups() }
64+
val alixGroup = alixClient.conversations.listGroups().first()
65+
assert(boGroup.id.isNotEmpty())
66+
assert(alixGroup.id.isNotEmpty())
67+
68+
alixGroup.addMembers(listOf(caro.walletAddress))
69+
runBlocking { boGroup.sync() }
70+
assertEquals(alixGroup.memberAddresses().size, 3)
71+
assertEquals(boGroup.memberAddresses().size, 3)
72+
73+
alixGroup.removeMembers(listOf(caro.walletAddress))
74+
runBlocking { boGroup.sync() }
75+
assertEquals(alixGroup.memberAddresses().size, 2)
76+
assertEquals(boGroup.memberAddresses().size, 2)
77+
78+
boGroup.addMembers(listOf(caro.walletAddress))
79+
runBlocking { alixGroup.sync() }
80+
assertEquals(alixGroup.memberAddresses().size, 3)
81+
assertEquals(boGroup.memberAddresses().size, 3)
82+
}
83+
84+
@Test
85+
fun testCanCreateAGroupWithAdminPermissions() {
86+
val boGroup = boClient.conversations.newGroup(
87+
listOf(alix.walletAddress),
88+
permissions = GroupPermissions.GROUP_CREATOR_IS_ADMIN
89+
)
90+
runBlocking { alixClient.conversations.syncGroups() }
91+
val alixGroup = alixClient.conversations.listGroups().first()
92+
assert(boGroup.id.isNotEmpty())
93+
assert(alixGroup.id.isNotEmpty())
94+
95+
boGroup.addMembers(listOf(caro.walletAddress))
96+
runBlocking { alixGroup.sync() }
97+
assertEquals(alixGroup.memberAddresses().size, 3)
98+
assertEquals(boGroup.memberAddresses().size, 3)
99+
100+
assertThrows(XMTPException::class.java) {
101+
alixGroup.removeMembers(listOf(caro.walletAddress))
102+
}
103+
runBlocking { boGroup.sync() }
104+
assertEquals(alixGroup.memberAddresses().size, 3)
105+
assertEquals(boGroup.memberAddresses().size, 3)
106+
107+
boGroup.removeMembers(listOf(caro.walletAddress))
108+
runBlocking { alixGroup.sync() }
109+
assertEquals(alixGroup.memberAddresses().size, 2)
110+
assertEquals(boGroup.memberAddresses().size, 2)
111+
112+
assertThrows(XMTPException::class.java) {
113+
alixGroup.addMembers(listOf(caro.walletAddress))
114+
}
115+
runBlocking { boGroup.sync() }
116+
assertEquals(alixGroup.memberAddresses().size, 2)
117+
assertEquals(boGroup.memberAddresses().size, 2)
63118
}
64119

65120
@Test
@@ -241,22 +296,25 @@ class GroupTest {
241296
@Test
242297
fun testCanStreamGroupMessages() = kotlinx.coroutines.test.runTest {
243298
val group = boClient.conversations.newGroup(listOf(alix.walletAddress.lowercase()))
299+
alixClient.conversations.syncGroups()
300+
val alixGroup = alixClient.conversations.listGroups().first()
244301
group.streamMessages().test {
245-
group.send("hi")
302+
alixGroup.send("hi")
246303
assertEquals("hi", awaitItem().body)
247-
group.send("hi again")
304+
alixGroup.send("hi again")
248305
assertEquals("hi again", awaitItem().body)
249306
}
250307
}
251308

252309
@Test
253310
fun testCanStreamDecryptedGroupMessages() = kotlinx.coroutines.test.runTest {
254311
val group = boClient.conversations.newGroup(listOf(alix.walletAddress))
255-
312+
alixClient.conversations.syncGroups()
313+
val alixGroup = alixClient.conversations.listGroups().first()
256314
group.streamDecryptedMessages().test {
257-
group.send("hi")
315+
alixGroup.send("hi")
258316
assertEquals("hi", awaitItem().encodedContent.content.toStringUtf8())
259-
group.send("hi again")
317+
alixGroup.send("hi again")
260318
assertEquals("hi again", awaitItem().encodedContent.content.toStringUtf8())
261319
}
262320
}

library/src/main/java/org/xmtp/android/library/Client.kt

+44-29
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ data class ClientOptions(
6666
val preEnableIdentityCallback: PreEventCallback? = null,
6767
val appContext: Context? = null,
6868
val enableAlphaMls: Boolean = false,
69+
val dbPath: String? = null,
70+
val dbEncryptionKey: ByteArray? = null,
6971
) {
7072
data class Api(
7173
val env: XMTPEnvironment = XMTPEnvironment.DEV,
@@ -81,8 +83,8 @@ class Client() {
8183
lateinit var contacts: Contacts
8284
lateinit var conversations: Conversations
8385
var logger: XMTPLogger = XMTPLogger()
84-
var libXMTPClient: FfiXmtpClient? = null
8586
val libXMTPVersion: String = getVersionInfo()
87+
private var libXMTPClient: FfiXmtpClient? = null
8688

8789
companion object {
8890
private const val TAG = "Client"
@@ -296,40 +298,52 @@ class Client() {
296298
if (isAlphaMlsEnabled(options)) {
297299
val alias = "xmtp-${options!!.api.env}-${accountAddress.lowercase()}"
298300

299-
val dbDir = File(appContext?.filesDir?.absolutePath, "xmtp_db")
300-
dbDir.mkdir()
301-
val dbPath: String = dbDir.absolutePath + "/$alias.db3"
302-
303-
val keyStore = KeyStore.getInstance("AndroidKeyStore")
304-
withContext(Dispatchers.IO) {
305-
keyStore.load(null)
301+
val dbPath = if (options.dbPath == null) {
302+
val dbDir = File(appContext?.filesDir?.absolutePath, "xmtp_db")
303+
dbDir.mkdir()
304+
dbDir.absolutePath + "/$alias.db3"
305+
} else {
306+
options.dbPath
306307
}
307308

308-
val entry = keyStore.getEntry(alias, null)
309-
310-
val retrievedKey: SecretKey = if (entry is KeyStore.SecretKeyEntry) {
311-
entry.secretKey
309+
val encryptionKey = if (options.dbEncryptionKey == null) {
310+
val keyStore = KeyStore.getInstance("AndroidKeyStore")
311+
withContext(Dispatchers.IO) {
312+
keyStore.load(null)
313+
}
314+
315+
val entry = keyStore.getEntry(alias, null)
316+
317+
val retrievedKey: SecretKey = if (entry is KeyStore.SecretKeyEntry) {
318+
entry.secretKey
319+
} else {
320+
val keyGenerator =
321+
KeyGenerator.getInstance(
322+
KeyProperties.KEY_ALGORITHM_AES,
323+
"AndroidKeyStore"
324+
)
325+
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
326+
alias,
327+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
328+
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
329+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
330+
.setKeySize(256)
331+
.build()
332+
333+
keyGenerator.init(keyGenParameterSpec)
334+
keyGenerator.generateKey()
335+
}
336+
retrievedKey.encoded
312337
} else {
313-
val keyGenerator =
314-
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
315-
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
316-
alias,
317-
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
318-
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
319-
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
320-
.setKeySize(256)
321-
.build()
322-
323-
keyGenerator.init(keyGenParameterSpec)
324-
keyGenerator.generateKey()
338+
options.dbEncryptionKey
325339
}
326340

327341
createClient(
328342
logger = logger,
329343
host = if (options.api.env == XMTPEnvironment.LOCAL) "http://${options.api.env.getValue()}:5556" else "https://${options.api.env.getValue()}:443",
330344
isSecure = options.api.isSecure,
331345
db = dbPath,
332-
encryptionKey = retrievedKey.encoded,
346+
encryptionKey = encryptionKey,
333347
accountAddress = accountAddress,
334348
legacyIdentitySource = legacyIdentitySource,
335349
legacySignedPrivateKeyProto = privateKeyBundleV1.toV2().identityKey.toByteArray()
@@ -564,11 +578,12 @@ class Client() {
564578
return runBlocking { query(Topic.contact(peerAddress)).envelopesList.size > 0 }
565579
}
566580

567-
fun canMessage(addresses: List<String>): Boolean {
568-
return runBlocking {
569-
libXMTPClient != null && !libXMTPClient!!.canMessage(addresses.map { it })
570-
.contains(false)
581+
fun canMessageV3(addresses: List<String>): Boolean {
582+
if (libXMTPClient == null) return false
583+
val statuses = runBlocking {
584+
libXMTPClient!!.canMessage(addresses)
571585
}
586+
return !statuses.contains(false)
572587
}
573588

574589
val privateKeyBundle: PrivateKeyBundle

library/src/main/java/org/xmtp/android/library/Conversations.kt

+7-3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import uniffi.xmtpv3.FfiConversationCallback
3939
import uniffi.xmtpv3.FfiConversations
4040
import uniffi.xmtpv3.FfiGroup
4141
import uniffi.xmtpv3.FfiListConversationsOptions
42+
import uniffi.xmtpv3.GroupPermissions
4243
import java.util.Date
4344
import kotlin.time.Duration.Companion.nanoseconds
4445
import kotlin.time.DurationUnit
@@ -91,7 +92,10 @@ data class Conversations(
9192
)
9293
}
9394

94-
fun newGroup(accountAddresses: List<String>): Group {
95+
fun newGroup(
96+
accountAddresses: List<String>,
97+
permissions: GroupPermissions = GroupPermissions.EVERYONE_IS_ADMIN,
98+
): Group {
9599
if (accountAddresses.isEmpty()) {
96100
throw XMTPException("Cannot start an empty group chat.")
97101
}
@@ -100,12 +104,12 @@ data class Conversations(
100104
) {
101105
throw XMTPException("Recipient is sender")
102106
}
103-
if (!client.canMessage(accountAddresses)) {
107+
if (!client.canMessageV3(accountAddresses)) {
104108
throw XMTPException("Recipient not on network")
105109
}
106110

107111
val group = runBlocking {
108-
libXMTPConversations?.createGroup(accountAddresses, permissions = null)
112+
libXMTPConversations?.createGroup(accountAddresses, permissions = permissions)
109113
?: throw XMTPException("Client does not support Groups")
110114
}
111115
client.contacts.allowGroup(groupIds = listOf(group.id()))

library/src/main/java/org/xmtp/android/library/Group.kt

+11-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import uniffi.xmtpv3.FfiGroup
1515
import uniffi.xmtpv3.FfiListMessagesOptions
1616
import uniffi.xmtpv3.FfiMessage
1717
import uniffi.xmtpv3.FfiMessageCallback
18+
import java.lang.Exception
1819
import java.util.Date
1920
import kotlin.time.Duration.Companion.nanoseconds
2021
import kotlin.time.DurationUnit
@@ -136,11 +137,19 @@ class Group(val client: Client, private val libXMTPGroup: FfiGroup) {
136137
}
137138

138139
fun addMembers(addresses: List<String>) {
139-
runBlocking { libXMTPGroup.addMembers(addresses) }
140+
try {
141+
runBlocking { libXMTPGroup.addMembers(addresses) }
142+
} catch (e: Exception) {
143+
throw XMTPException("User does not have permissions", e)
144+
}
140145
}
141146

142147
fun removeMembers(addresses: List<String>) {
143-
runBlocking { libXMTPGroup.removeMembers(addresses) }
148+
try {
149+
runBlocking { libXMTPGroup.removeMembers(addresses) }
150+
} catch (e: Exception) {
151+
throw XMTPException("User does not have permissions", e)
152+
}
144153
}
145154

146155
fun memberAddresses(): List<String> {

0 commit comments

Comments
 (0)