Skip to content

Commit

Permalink
Merge pull request #231 from xmtp/cv/group-spike
Browse files Browse the repository at this point in the history
Create LibXMTP Client - android
  • Loading branch information
cameronvoell authored Feb 6, 2024
2 parents 74ffe29 + a68fe27 commit 6a71abb
Show file tree
Hide file tree
Showing 11 changed files with 2,488 additions and 1,971 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,7 @@ The `env` parameter accepts one of three valid values: `dev`, `production`, or `
- `local`: Use to have a client communicate with an XMTP node you are running locally. For example, an XMTP node developer can set `env` to `local` to generate client traffic to test a node running locally.

The `production` network is configured to store messages indefinitely. XMTP may occasionally delete messages and keys from the `dev` network, and will provide advance notice in the [XMTP Discord community](https://discord.gg/xmtp).

## Enabling group chat

Coming soon...
13 changes: 12 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,19 @@ repositories {
dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation "org.xmtp:android:0.7.6"
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.facebook.react:react-native:0.71.3'
implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1"
implementation "org.xmtp:android:0.7.9"
// xmtp-android local testing setup below (comment org.xmtp:android above)
// implementation files('<PATH TO XMTP-ANDROID>/xmtp-android/library/build/outputs/aar/library-debug.aar')
// implementation 'com.google.crypto.tink:tink-android:1.7.0'
// implementation 'io.grpc:grpc-kotlin-stub:1.3.0'
// implementation 'io.grpc:grpc-okhttp:1.51.1'
// implementation 'io.grpc:grpc-protobuf-lite:1.51.0'
// implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
// implementation 'org.web3j:crypto:5.0.0'
// implementation "net.java.dev.jna:jna:5.13.0@aar"
// implementation 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
// implementation 'org.xmtp:proto-kotlin:3.40.1'
}
146 changes: 139 additions & 7 deletions android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package expo.modules.xmtpreactnativesdk

import android.content.Context
import android.net.Uri
import android.util.Base64
import android.util.Base64.NO_WRAP
import android.util.Log
import androidx.core.net.toUri
import com.google.gson.JsonParser
import com.google.protobuf.kotlin.toByteString
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper
Expand All @@ -16,17 +18,20 @@ import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper
import expo.modules.xmtpreactnativesdk.wrappers.PreparedLocalMessage
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import org.json.JSONObject
import org.xmtp.android.library.Client
import org.xmtp.android.library.ClientOptions
import org.xmtp.android.library.Conversation
import org.xmtp.android.library.Group
import org.xmtp.android.library.PreEventCallback
import org.xmtp.android.library.PreparedMessage
import org.xmtp.android.library.SendOptions
Expand Down Expand Up @@ -97,7 +102,15 @@ fun Conversation.cacheKey(clientAddress: String): String {
return "${clientAddress}:${topic}"
}

fun Group.cacheKey(clientAddress: String): String {
return "${clientAddress}:${id}"
}

class XMTPModule : Module() {

val context: Context
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()

private fun apiEnvironments(env: String, appVersion: String?): ClientOptions.Api {
return when (env) {
"local" -> ClientOptions.Api(
Expand Down Expand Up @@ -125,6 +138,7 @@ class XMTPModule : Module() {
private var signer: ReactNativeSigner? = null
private val isDebugEnabled = BuildConfig.DEBUG // TODO: consider making this configurable
private val conversations: MutableMap<String, Conversation> = mutableMapOf()
private val groups: MutableMap<String, Group> = mutableMapOf()
private val subscriptions: MutableMap<String, Job> = mutableMapOf()
private var preEnableIdentityCallbackDeferred: CompletableDeferred<Unit>? = null
private var preCreateIdentityCallbackDeferred: CompletableDeferred<Unit>? = null
Expand All @@ -150,8 +164,9 @@ class XMTPModule : Module() {
//
// Auth functions
//
AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean? ->
AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean? ->
logV("auth")
requireLocalEnvForAlphaMLS(enableAlphaMls, environment)
val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address)
signer = reactSigner

Expand All @@ -163,10 +178,14 @@ class XMTPModule : Module() {
preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true }
val preEnableIdentityCallback: PreEventCallback? =
preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true }
val context = if (enableAlphaMls == true) context else null

val options = ClientOptions(
api = apiEnvironments(environment, appVersion),
preCreateIdentityCallback = preCreateIdentityCallback,
preEnableIdentityCallback = preEnableIdentityCallback
preEnableIdentityCallback = preEnableIdentityCallback,
enableAlphaMls = enableAlphaMls == true,
appContext = context
)
clients[address] = Client().create(account = reactSigner, options = options)
ContentJson.Companion
Expand All @@ -180,8 +199,9 @@ class XMTPModule : Module() {
}

// Generate a random wallet and set the client to that
AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean? ->
AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean? ->
logV("createRandom")
requireLocalEnvForAlphaMLS(enableAlphaMls, environment)
val privateKey = PrivateKeyBuilder()

if (hasCreateIdentityCallback == true)
Expand All @@ -192,22 +212,31 @@ class XMTPModule : Module() {
preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true }
val preEnableIdentityCallback: PreEventCallback? =
preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true }
val context = if (enableAlphaMls == true) context else null

val options = ClientOptions(
api = apiEnvironments(environment, appVersion),
preCreateIdentityCallback = preCreateIdentityCallback,
preEnableIdentityCallback = preEnableIdentityCallback
preEnableIdentityCallback = preEnableIdentityCallback,
enableAlphaMls = enableAlphaMls == true,
appContext = context
)
val randomClient = Client().create(account = privateKey, options = options)
ContentJson.Companion
clients[randomClient.address] = randomClient
randomClient.address
}

AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String? ->
AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String?, enableAlphaMls: Boolean? ->
logV("createFromKeyBundle")
requireLocalEnvForAlphaMLS(enableAlphaMls, environment)
try {
logV("createFromKeyBundle")
val options = ClientOptions(api = apiEnvironments(environment, appVersion))
val context = if (enableAlphaMls == true) context else null
val options = ClientOptions(
api = apiEnvironments(environment, appVersion),
enableAlphaMls = enableAlphaMls == true,
appContext = context
)
val bundle =
PrivateKeyOuterClass.PrivateKeyBundle.parseFrom(
Base64.decode(
Expand Down Expand Up @@ -352,6 +381,16 @@ class XMTPModule : Module() {
}
}

AsyncFunction("listGroups") { clientAddress: String ->
logV("listGroups")
val client = clients[clientAddress] ?: throw XMTPException("No client")
val groupList = client.conversations.listGroups()
groupList.map { group ->
groups[group.cacheKey(clientAddress)] = group
GroupWrapper.encode(client, group)
}
}

AsyncFunction("loadMessages") { clientAddress: String, topic: String, limit: Int?, before: Long?, after: Long?, direction: String? ->
logV("loadMessages")
val conversation =
Expand All @@ -373,6 +412,16 @@ class XMTPModule : Module() {
.map { DecodedMessageWrapper.encode(it) }
}

AsyncFunction("groupMessages") { clientAddress: String, id: String ->
logV("groupMessages")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to synch groups")
}
val group = findGroup(clientAddress, id)
group?.decryptedMessages()?.map { DecodedMessageWrapper.encode(it) }
}

AsyncFunction("loadBatchMessages") { clientAddress: String, topics: List<String> ->
logV("loadBatchMessages")
val client = clients[clientAddress] ?: throw XMTPException("No client")
Expand Down Expand Up @@ -433,6 +482,21 @@ class XMTPModule : Module() {
)
}

AsyncFunction("sendMessageToGroup") { clientAddress: String, idString: String, contentJson: String ->
logV("sendMessageToGroup")
val group =
findGroup(
clientAddress = clientAddress,
idString = idString
)
?: throw XMTPException("no group found for $idString")
val sending = ContentJson.fromJson(contentJson)
group.send(
content = sending.content,
options = SendOptions(contentType = sending.type)
)
}

AsyncFunction("prepareMessage") { clientAddress: String, conversationTopic: String, contentJson: String ->
logV("prepareMessage")
val conversation =
Expand Down Expand Up @@ -531,6 +595,47 @@ class XMTPModule : Module() {
ConversationWrapper.encode(client, conversation)
}

AsyncFunction("createGroup") { clientAddress: String, peerAddresses: List<String> ->
logV("createGroup")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to create a group")
}
val group = client.conversations.newGroup(peerAddresses)
logV("id after creating group: " + Base64.encodeToString(group.id, NO_WRAP))
val encodedGroup = GroupWrapper.encode(client, group)
return@AsyncFunction encodedGroup
}

AsyncFunction("listMemberAddresses") { clientAddress: String, groupId: String ->
logV("listMembers")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to create a group")
}
val group = findGroup(clientAddress, groupId)
return@AsyncFunction group?.memberAddresses()
}

AsyncFunction("syncGroups") { clientAddress: String ->
logV("syncGroups")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to synch groups")
}
runBlocking { client.conversations.syncGroups() }
}

AsyncFunction("syncGroup") { clientAddress: String, id: String ->
logV("syncGroup")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to synch groups")
}
val group = findGroup(clientAddress, id)
runBlocking { group?.sync() }
}

Function("subscribeToConversations") { clientAddress: String ->
logV("subscribeToConversations")
subscribeToConversations(clientAddress = clientAddress)
Expand Down Expand Up @@ -673,6 +778,27 @@ class XMTPModule : Module() {
return null
}

private fun findGroup(
clientAddress: String,
idString: String,
): Group? {
val client = clients[clientAddress] ?: throw XMTPException("No client")

val cacheKey = "${clientAddress}:${idString}"
val cacheGroup = groups[cacheKey]
if (cacheGroup != null) {
return cacheGroup
} else {
val group = client.conversations.listGroups()
.firstOrNull { Base64.encodeToString(it.id, NO_WRAP) == idString }
if (group != null) {
groups[group.cacheKey(clientAddress)] = group
return group
}
}
return null
}

private fun subscribeToConversations(clientAddress: String) {
val client = clients[clientAddress] ?: throw XMTPException("No client")

Expand Down Expand Up @@ -780,6 +906,12 @@ class XMTPModule : Module() {
preCreateIdentityCallbackDeferred?.await()
preCreateIdentityCallbackDeferred = null
}

private fun requireLocalEnvForAlphaMLS(enableAlphaMls: Boolean?, environment: String) {
if (enableAlphaMls == true && environment != "local") {
throw XMTPException("Environment must be \"local\" to enable alpha MLS")
}
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package expo.modules.xmtpreactnativesdk.wrappers

import android.util.Base64
import android.util.Base64.NO_WRAP
import com.google.gson.GsonBuilder
import org.xmtp.android.library.Client
import org.xmtp.android.library.Group

class GroupWrapper {

companion object {
private fun encodeToObj(client: Client, group: Group, idString: String): Map<String, Any> {
return mapOf(
"clientAddress" to client.address,
"id" to idString,
"createdAt" to group.createdAt.time,
"peerAddresses" to group.memberAddresses(),

)
}

fun encode(client: Client, group: Group): String {
val gson = GsonBuilder().create()
val obj = encodeToObj(client, group, Base64.encodeToString(group.id, NO_WRAP))
return gson.toJson(obj)
}
}
}
Loading

0 comments on commit 6a71abb

Please sign in to comment.