Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V3 HMAC key support for self push notifications #582

Merged
merged 12 commits into from
Jan 8, 2025
6 changes: 6 additions & 0 deletions .changeset/hungry-onions-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@xmtp/react-native-sdk": patch
---

V3 HMAC key support for self push notifications
Streaming preference updates
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ repositories {
dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation "org.xmtp:android:3.0.19"
implementation "org.xmtp:android:3.0.20"
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"
Expand Down
110 changes: 84 additions & 26 deletions android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import expo.modules.xmtpreactnativesdk.wrappers.ContentJson
import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper
import expo.modules.xmtpreactnativesdk.wrappers.ConversationParamsWrapper
import expo.modules.xmtpreactnativesdk.wrappers.CreateGroupParamsWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper
import expo.modules.xmtpreactnativesdk.wrappers.MessageWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.DmWrapper
import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment
Expand All @@ -42,6 +42,7 @@ import org.xmtp.android.library.Conversation
import org.xmtp.android.library.Conversations.*
import org.xmtp.android.library.EntryType
import org.xmtp.android.library.PreEventCallback
import org.xmtp.android.library.PreferenceType
import org.xmtp.android.library.SendOptions
import org.xmtp.android.library.SigningKey
import org.xmtp.android.library.WalletType
Expand All @@ -54,13 +55,13 @@ import org.xmtp.android.library.codecs.EncryptedEncodedContent
import org.xmtp.android.library.codecs.RemoteAttachment
import org.xmtp.android.library.codecs.decoded
import org.xmtp.android.library.hexToByteArray
import org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration
import org.xmtp.android.library.libxmtp.Message
import org.xmtp.android.library.libxmtp.PermissionOption
import org.xmtp.android.library.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.Signature
import org.xmtp.android.library.push.Service
import org.xmtp.android.library.push.XMTPPush
import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration
import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
Expand Down Expand Up @@ -208,6 +209,7 @@ class XMTPModule : Module() {
"message",
"conversationMessage",
"consent",
"preferences",
)

Function("address") { installationId: String ->
Expand Down Expand Up @@ -520,15 +522,13 @@ class XMTPModule : Module() {
).toJson()
}

AsyncFunction("listGroups") Coroutine { installationId: String, groupParams: String?, sortOrder: String?, limit: Int?, consentState: String? ->
AsyncFunction("listGroups") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentState: String? ->
withContext(Dispatchers.IO) {
logV("listGroups")
val client = clients[installationId] ?: throw XMTPException("No client")
val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "")
val order = getConversationSortOrder(sortOrder ?: "")
val consent = consentState?.let { getConsentState(it) }
val groups = client.conversations.listGroups(
order = order,
limit = limit,
consentState = consent
)
Expand All @@ -538,15 +538,13 @@ class XMTPModule : Module() {
}
}

AsyncFunction("listDms") Coroutine { installationId: String, groupParams: String?, sortOrder: String?, limit: Int?, consentState: String? ->
AsyncFunction("listDms") Coroutine { installationId: String, groupParams: String?, limit: Int?, consentState: String? ->
withContext(Dispatchers.IO) {
logV("listDms")
val client = clients[installationId] ?: throw XMTPException("No client")
val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "")
val order = getConversationSortOrder(sortOrder ?: "")
val consent = consentState?.let { getConsentState(it) }
val dms = client.conversations.listDms(
order = order,
limit = limit,
consentState = consent
)
Expand All @@ -556,22 +554,29 @@ class XMTPModule : Module() {
}
}

AsyncFunction("listConversations") Coroutine { installationId: String, conversationParams: String?, sortOrder: String?, limit: Int?, consentState: String? ->
AsyncFunction("listConversations") Coroutine { installationId: String, conversationParams: String?, limit: Int?, consentState: String? ->
withContext(Dispatchers.IO) {
logV("listConversations")
val client = clients[installationId] ?: throw XMTPException("No client")
val params =
ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?: "")
val order = getConversationSortOrder(sortOrder ?: "")
val consent = consentState?.let { getConsentState(it) }
val conversations =
client.conversations.list(order = order, limit = limit, consentState = consent)
client.conversations.list(limit = limit, consentState = consent)
conversations.map { conversation ->
ConversationWrapper.encode(client, conversation, params)
}
}
}

AsyncFunction("getHmacKeys") { inboxId: String ->
logV("getHmacKeys")
val client = clients[inboxId] ?: throw XMTPException("No client")
val hmacKeys = client.conversations.getHmacKeys()
logV("$hmacKeys")
hmacKeys.toByteArray().map { it.toInt() and 0xFF }
}

AsyncFunction("conversationMessages") Coroutine { installationId: String, conversationId: String, limit: Int?, beforeNs: Long?, afterNs: Long?, direction: String? ->
withContext(Dispatchers.IO) {
logV("conversationMessages")
Expand All @@ -584,7 +589,7 @@ class XMTPModule : Module() {
direction = Message.SortDirection.valueOf(
direction ?: "DESCENDING"
)
)?.map { DecodedMessageWrapper.encode(it) }
)?.map { MessageWrapper.encode(it) }
}
}

Expand All @@ -594,7 +599,7 @@ class XMTPModule : Module() {
val client = clients[installationId] ?: throw XMTPException("No client")
val message = client.findMessage(messageId)
message?.let {
DecodedMessageWrapper.encode(it.decode())
MessageWrapper.encode(it)
}
}
}
Expand Down Expand Up @@ -1174,7 +1179,9 @@ class XMTPModule : Module() {
val conversation = client.findConversation(id)
?: throw XMTPException("no conversation found for $id")
val message = conversation.processMessage(Base64.decode(encryptedMessage, NO_WRAP))
DecodedMessageWrapper.encode(message.decode())
message?.let {
MessageWrapper.encode(it)
}
}
}

Expand Down Expand Up @@ -1257,6 +1264,12 @@ class XMTPModule : Module() {
}
}

Function("subscribeToPreferenceUpdates") { installationId: String ->
logV("subscribeToPreferenceUpdates")

subscribeToPreferenceUpdates(installationId = installationId)
}

Function("subscribeToConsent") { installationId: String ->
logV("subscribeToConsent")

Expand Down Expand Up @@ -1284,6 +1297,11 @@ class XMTPModule : Module() {
}
}

Function("unsubscribeFromPreferenceUpdates") { installationId: String ->
logV("unsubscribeFromPreferenceUpdates")
subscriptions[getPreferenceUpdatesKey(installationId)]?.cancel()
}

Function("unsubscribeFromConsent") { installationId: String ->
logV("unsubscribeFromConsent")
subscriptions[getConsentKey(installationId)]?.cancel()
Expand Down Expand Up @@ -1315,15 +1333,29 @@ class XMTPModule : Module() {
xmtpPush?.register(token)
}

Function("subscribePushTopics") { topics: List<String> ->
Function("subscribePushTopics") { installationId: String, topics: List<String> ->
logV("subscribePushTopics")
if (topics.isNotEmpty()) {
if (xmtpPush == null) {
throw XMTPException("Push server not registered")
}
val client = clients[installationId] ?: throw XMTPException("No client")

val hmacKeysResult = client.conversations.getHmacKeys()
val subscriptions = topics.map {
val hmacKeys = hmacKeysResult.hmacKeysMap
val result = hmacKeys[it]?.valuesList?.map { hmacKey ->
Service.Subscription.HmacKey.newBuilder().also { sub_key ->
sub_key.key = hmacKey.hmacKey
sub_key.thirtyDayPeriodsSinceEpoch = hmacKey.thirtyDayPeriodsSinceEpoch
}.build()
}

Service.Subscription.newBuilder().also { sub ->
sub.addAllHmacKeys(result)
if (!result.isNullOrEmpty()) {
sub.addAllHmacKeys(result)
}
sub.topic = it
}.build()
}
Expand Down Expand Up @@ -1390,13 +1422,6 @@ class XMTPModule : Module() {
}
}

private fun getConversationSortOrder(order: String): ConversationOrder {
return when (order) {
"lastMessage" -> ConversationOrder.LAST_MESSAGE
else -> ConversationOrder.CREATED_AT
}
}

private fun consentStateToString(state: ConsentState): String {
return when (state) {
ConsentState.ALLOWED -> "allowed"
Expand All @@ -1405,6 +1430,35 @@ class XMTPModule : Module() {
}
}

private fun preferenceTypeToString(type: PreferenceType): String {
return when (type) {
PreferenceType.HMAC_KEYS -> "hmac_keys"
}
}

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

subscriptions[getPreferenceUpdatesKey(installationId)]?.cancel()
subscriptions[getPreferenceUpdatesKey(installationId)] =
CoroutineScope(Dispatchers.IO).launch {
try {
client.preferences.streamPreferenceUpdates().collect { type ->
sendEvent(
"preferences",
mapOf(
"installationId" to installationId,
"preferenceType" to preferenceTypeToString(type)
)
)
}
} catch (e: Exception) {
Log.e("XMTPModule", "Error in preference subscription: $e")
subscriptions[getPreferenceUpdatesKey(installationId)]?.cancel()
}
}
}

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

Expand All @@ -1422,7 +1476,7 @@ class XMTPModule : Module() {
)
}
} catch (e: Exception) {
Log.e("XMTPModule", "Error in group subscription: $e")
Log.e("XMTPModule", "Error in consent subscription: $e")
subscriptions[getConsentKey(installationId)]?.cancel()
}
}
Expand Down Expand Up @@ -1465,7 +1519,7 @@ class XMTPModule : Module() {
"message",
mapOf(
"installationId" to installationId,
"message" to DecodedMessageWrapper.encodeMap(message),
"message" to MessageWrapper.encodeMap(message),
)
)
}
Expand All @@ -1489,7 +1543,7 @@ class XMTPModule : Module() {
"conversationMessage",
mapOf(
"installationId" to installationId,
"message" to DecodedMessageWrapper.encodeMap(message),
"message" to MessageWrapper.encodeMap(message),
"conversationId" to id,
)
)
Expand All @@ -1501,6 +1555,10 @@ class XMTPModule : Module() {
}
}

private fun getPreferenceUpdatesKey(installationId: String): String {
return "preferences:$installationId"
}

private fun getConsentKey(installationId: String): String {
return "consent:$installationId"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ class DmWrapper {
put("consentState", consentStateToString(dm.consentState()))
}
if (dmParams.lastMessage) {
val lastMessage = dm.messages(limit = 1).firstOrNull()
val lastMessage = dm.lastMessage()
if (lastMessage != null) {
put("lastMessage", DecodedMessageWrapper.encode(lastMessage))
put("lastMessage", MessageWrapper.encode(lastMessage))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ class GroupWrapper {
put("consentState", consentStateToString(group.consentState()))
}
if (groupParams.lastMessage) {
val lastMessage = group.messages(limit = 1).firstOrNull()
val lastMessage = group.lastMessage()
if (lastMessage != null) {
put("lastMessage", DecodedMessageWrapper.encode(lastMessage))
put("lastMessage", MessageWrapper.encode(lastMessage))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package expo.modules.xmtpreactnativesdk.wrappers

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.InboxState
import org.xmtp.android.library.libxmtp.InboxState

class InboxStateWrapper {
companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package expo.modules.xmtpreactnativesdk.wrappers

import com.google.gson.GsonBuilder
import org.xmtp.android.library.DecodedMessage
import org.xmtp.android.library.codecs.description
import org.xmtp.android.library.libxmtp.Message

class DecodedMessageWrapper {
class MessageWrapper {

companion object {
fun encode(model: DecodedMessage): String {
fun encode(model: Message): String {
val gson = GsonBuilder().create()
val message = encodeMap(model)
return gson.toJson(message)
}

fun encodeMap(model: DecodedMessage): Map<String, Any?> {
fun encodeMap(model: Message): Map<String, Any?> {
// Kotlin/Java Protos don't support null values and will always put the default ""
// Check if there is a fallback, if there is then make it the set fallback, if not null
val fallback = if (model.encodedContent.hasFallback()) model.encodedContent.fallback else null
Expand All @@ -23,7 +23,7 @@ class DecodedMessageWrapper {
"contentTypeId" to model.encodedContent.type.description,
"content" to ContentJson(model.encodedContent).toJsonMap(),
"senderInboxId" to model.senderInboxId,
"sentNs" to model.sentNs,
"sentNs" to model.sentAtNs,
"fallback" to fallback,
"deliveryStatus" to model.deliveryStatus.toString()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package expo.modules.xmtpreactnativesdk.wrappers

import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption
import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionPolicySet
import org.xmtp.android.library.libxmtp.PermissionOption
import org.xmtp.android.library.libxmtp.PermissionPolicySet

class PermissionPolicySetWrapper {

Expand Down
Loading
Loading