Skip to content

Commit

Permalink
Merge pull request #582 from xmtp/np/hmac-keys-v3
Browse files Browse the repository at this point in the history
V3 HMAC key support for self push notifications
  • Loading branch information
nplasterer authored Jan 8, 2025
2 parents 9d2c5e7 + fc50cab commit 34ce0f4
Show file tree
Hide file tree
Showing 23 changed files with 444 additions and 217 deletions.
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

0 comments on commit 34ce0f4

Please sign in to comment.