Skip to content

Commit

Permalink
fix: Android V3 Build (#1217)
Browse files Browse the repository at this point in the history
Fix Android Build
Update Conversation List to have hooks
  • Loading branch information
alexrisch committed Nov 19, 2024
1 parent 03a55dc commit cffc384
Show file tree
Hide file tree
Showing 12 changed files with 541 additions and 467 deletions.
35 changes: 35 additions & 0 deletions android/app/src/main/java/com/converse/Preferences.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import org.xmtp.android.library.ConsentList
import org.xmtp.android.library.ConsentState
import org.xmtp.android.library.Conversation

suspend fun isConversationBlocked(conversation: Conversation, consentList: ConsentList): Boolean {
return isConversationIdBlocked(conversation.id, consentList)
}

suspend fun isConversationAllowed(conversation: Conversation, consentList: ConsentList): Boolean {
return isConversationIdAllowed(conversation.id, consentList)
}

suspend fun isConversationIdBlocked(conversationId: String, consentList: ConsentList): Boolean {
return consentList.conversationState(conversationId) == ConsentState.DENIED
}

suspend fun isConversationIdAllowed(conversationId: String, consentList: ConsentList): Boolean {
return consentList.conversationState(conversationId) == ConsentState.ALLOWED
}

suspend fun isAddressAllowed(address: String, consentList: ConsentList): Boolean {
return consentList.addressState(address) == ConsentState.ALLOWED
}

suspend fun isAddressBlocked(address: String, consentList: ConsentList): Boolean {
return consentList.addressState(address) == ConsentState.DENIED
}

suspend fun isInboxIdAllowed(inboxId: String, consentList: ConsentList): Boolean {
return consentList.inboxIdState(inboxId) == ConsentState.ALLOWED
}

suspend fun isInboxIdBlocked(inboxId: String, consentList: ConsentList): Boolean {
return consentList.inboxIdState(inboxId) == ConsentState.DENIED
}
82 changes: 18 additions & 64 deletions android/app/src/main/java/com/converse/PushNotificationsService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,9 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.beust.klaxon.Klaxon
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.converse.xmtp.NotificationDataResult
import com.converse.xmtp.getGroup
import com.converse.xmtp.getNewConversationFromEnvelope
import com.converse.xmtp.getNewGroup
import com.converse.xmtp.getXmtpClient
import com.converse.xmtp.handleGroupMessage
import com.converse.xmtp.handleGroupWelcome
import com.converse.xmtp.handleNewConversationFirstMessage
import com.converse.xmtp.handleOngoingConversationMessage
import com.converse.xmtp.initCodecs
import com.facebook.react.bridge.ReactApplicationContext
import com.google.crypto.tink.subtle.Base64
Expand All @@ -32,26 +24,18 @@ import expo.modules.kotlin.ModulesProvider
import expo.modules.kotlin.modules.Module
import expo.modules.notifications.notifications.JSONNotificationContentBuilder
import expo.modules.notifications.notifications.model.Notification
import expo.modules.notifications.notifications.model.NotificationAction
import expo.modules.notifications.notifications.model.NotificationContent
import expo.modules.notifications.notifications.model.NotificationRequest
import expo.modules.notifications.notifications.model.NotificationResponse
import expo.modules.notifications.notifications.model.triggers.FirebaseNotificationTrigger
import expo.modules.notifications.notifications.presentation.builders.CategoryAwareNotificationBuilder
import expo.modules.notifications.notifications.presentation.builders.ExpoNotificationBuilder
import expo.modules.notifications.service.NotificationsService.Companion.EVENT_TYPE_KEY
import expo.modules.notifications.service.NotificationsService.Companion.NOTIFICATION_ACTION_KEY
import expo.modules.notifications.service.NotificationsService.Companion.NOTIFICATION_EVENT_ACTION
import expo.modules.notifications.service.NotificationsService.Companion.NOTIFICATION_KEY
import expo.modules.notifications.service.NotificationsService.Companion.findDesignatedBroadcastReceiver
import expo.modules.notifications.service.delegates.SharedPreferencesNotificationCategoriesStore
import expo.modules.securestore.AuthenticationHelper
import expo.modules.securestore.SecureStoreModule
import expo.modules.securestore.encryptors.AESEncryptor
import expo.modules.securestore.encryptors.HybridAESEncryptor
import kotlinx.coroutines.*
import org.json.JSONObject
import org.xmtp.android.library.messages.EnvelopeBuilder
import java.lang.ref.WeakReference
import java.security.KeyStore
import java.util.*
Expand All @@ -60,6 +44,12 @@ import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import com.converse.xmtp.getNewConversation
import com.converse.xmtp.handleV3Message
import com.converse.xmtp.handleV3Welcome
import com.google.protobuf.kotlin.toByteString
import org.xmtp.proto.message.api.v1.MessageApiOuterClass
import org.xmtp.proto.message.api.v1.MessageApiOuterClass.Envelope

class PushNotificationsService : FirebaseMessagingService() {
companion object {
Expand Down Expand Up @@ -110,11 +100,11 @@ class PushNotificationsService : FirebaseMessagingService() {
Log.d(TAG, "INSTANTIATED XMTP CLIENT FOR ${notificationData.contentTopic}")

val encryptedMessageData = Base64.decode(notificationData.message, Base64.NO_WRAP)
val envelope = EnvelopeBuilder.buildFromString(
notificationData.contentTopic,
Date(notificationData.timestampNs.toLong() / 1000000),
encryptedMessageData
)
val envelope = Envelope.newBuilder().apply {
timestampNs = notificationData.timestampNs.toLong() / 1_000_000
message = encryptedMessageData.toByteString()// Convert ByteString to byte array
contentTopic = notificationData.contentTopic
}.build()

var shouldShowNotification = false
var result = NotificationDataResult()
Expand All @@ -130,38 +120,13 @@ class PushNotificationsService : FirebaseMessagingService() {
)
return@launch
}
if (isInviteTopic(notificationData.contentTopic)) {
Log.d(TAG, "Handling a new conversation notification")
val conversation =
getNewConversationFromEnvelope(applicationContext, xmtpClient, envelope)
if (conversation != null) {
result = handleNewConversationFirstMessage(
if (isV3WelcomeTopic(notificationData.contentTopic)) {
val convo = getNewConversation(xmtpClient, notificationData.contentTopic)
if (convo != null) {
result = handleV3Welcome(
applicationContext,
xmtpClient,
conversation,
remoteMessage
)
if (result != NotificationDataResult()) {
shouldShowNotification = result.shouldShowNotification
}

// Replace invite-topic with the topic in the notification content
val newNotificationData = NotificationData(
notificationData.message,
notificationData.timestampNs,
conversation.topic,
notificationData.account,
)
val newNotificationDataJson = Klaxon().toJsonString(newNotificationData)
remoteMessage.data["body"] = newNotificationDataJson
}
} else if (isV3WelcomeTopic(notificationData.contentTopic)) {
val group = getNewGroup(xmtpClient, notificationData.contentTopic)
if (group != null) {
result = handleGroupWelcome(
applicationContext,
xmtpClient,
group,
convo,
remoteMessage
)
if (result != NotificationDataResult()) {
Expand All @@ -170,22 +135,11 @@ class PushNotificationsService : FirebaseMessagingService() {
}
} else if (isV3MessageTopic(notificationData.contentTopic)) {
Log.d(TAG, "Handling an ongoing group message notification")
result = handleGroupMessage(
applicationContext,
xmtpClient,
envelope,
remoteMessage
)
if (result != NotificationDataResult()) {
shouldShowNotification = result.shouldShowNotification
}
} else {
Log.d(TAG, "Handling an ongoing conversation message notification")
result = handleOngoingConversationMessage(
result = handleV3Message(
applicationContext,
xmtpClient,
envelope,
remoteMessage
remoteMessage,
)
if (result != NotificationDataResult()) {
shouldShowNotification = result.shouldShowNotification
Expand Down
74 changes: 56 additions & 18 deletions android/app/src/main/java/com/converse/Spam.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import java.util.HashMap
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import org.web3j.crypto.Keys
import org.xmtp.android.library.Dm

val restrictedWords = listOf(
"Coinbase",
Expand Down Expand Up @@ -53,7 +54,7 @@ fun getMessageSpamScore(message: String?, contentType: String): Int {
return spamScore
}

fun computeSpamScore(address: String, message: String?, contentType: String, apiURI: String?, appContext: Context): Double {
fun computeDmSpamScore(address: String, message: String?, contentType: String, apiURI: String?, appContext: Context): Double {
var senderScore = runBlocking {
getSenderSpamScore(appContext, address, apiURI);
}
Expand All @@ -63,27 +64,28 @@ fun computeSpamScore(address: String, message: String?, contentType: String, api
suspend fun computeSpamScoreGroupMessage(client: Client, group: Group, decodedMessage: DecodedMessage, apiURI: String?): Int {
val senderSpamScore = 0
try {
client.contacts.refreshConsentList()
val groupDenied = client.contacts.isGroupDenied(group.id)
if (groupDenied) {
client.syncConsent()
val consentList = client.preferences.consentList
val groupBlocked = isConversationIdBlocked(group.id, consentList)
if (groupBlocked) {
// Network consent will override other checks
return 1
}

val senderInboxId = decodedMessage.senderAddress
val senderDenied = client.contacts.isInboxDenied(senderInboxId)
if (senderDenied) {
val senderBlocked = isInboxIdBlocked(senderInboxId, consentList)
if (senderBlocked) {
// Network consent will override other checks
return 1
}

val senderAllowed = client.contacts.isInboxAllowed(senderInboxId)
val senderAllowed = isInboxIdAllowed(senderInboxId, consentList)
if (senderAllowed) {
// Network consent will override other checks
return -1
}

val groupAllowed = client.contacts.isGroupAllowed(group.id)
val groupAllowed = isConversationIdAllowed(group.id, consentList)
if (groupAllowed) {
// Network consent will override other checks
return -1
Expand All @@ -92,12 +94,12 @@ suspend fun computeSpamScoreGroupMessage(client: Client, group: Group, decodedMe
val senderAddresses = group.members().find { it.inboxId == senderInboxId }?.addresses
if (senderAddresses != null) {
for (address in senderAddresses) {
if (client.contacts.isDenied(Keys.toChecksumAddress(address))) {
if (isAddressBlocked(Keys.toChecksumAddress(address), consentList)) {
return 1
}
}
for (address in senderAddresses) {
if (client.contacts.isAllowed(Keys.toChecksumAddress(address))) {
if (isAddressAllowed(Keys.toChecksumAddress(address), consentList)) {
return -1
}
}
Expand All @@ -121,22 +123,60 @@ suspend fun computeSpamScoreGroupMessage(client: Client, group: Group, decodedMe
return senderSpamScore + messageSpamScore
}

suspend fun computeSpamScoreDmWelcome(appContext: Context, client: Client, dm: Dm, apiURI: String?): Double {
try {
val consentList = client.preferences.consentList
// Probably an unlikely case until consent proofs for groups exist
val groupAllowed = isConversationIdAllowed(dm.id, consentList)
if (groupAllowed) {
return -1.0
}

val peerInboxId = dm.peerInboxId
val peerAllowed = isInboxIdAllowed(peerInboxId, consentList)
if (peerAllowed) {
return -1.0
}

val peerDenied = isInboxIdBlocked(peerInboxId, consentList)
if (peerDenied) {
return 1.0
}
val members = dm.members()
for (member in members) {
if (member.inboxId == peerInboxId) {
val firstAddress = member.addresses.first()
val senderSpamScore = getSenderSpamScore(
appContext = appContext,
address = Keys.toChecksumAddress(firstAddress),
apiURI = apiURI
)
return senderSpamScore
}
}
return 0.0
} catch (e: Exception) {
return 0.0
}
}


suspend fun computeSpamScoreGroupWelcome(appContext: Context, client: Client, group: Group, apiURI: String?): Double {
try {
client.contacts.refreshConsentList()
val consentList = client.preferences.consentList
// Probably an unlikely case until consent proofs for groups exist
val groupAllowed = client.contacts.isGroupAllowed(groupId = group.id)
val groupAllowed = isConversationIdAllowed(group.id, consentList)
if (groupAllowed) {
return -1.0
}

val inviterInboxId = group.addedByInboxId()
val inviterAllowed = client.contacts.isInboxAllowed(inboxId = inviterInboxId)
val inviterAllowed = isInboxIdAllowed(inviterInboxId, consentList)
if (inviterAllowed) {
return -1.0
}

val inviterDenied = client.contacts.isInboxDenied(inboxId = inviterInboxId)
val inviterDenied = isInboxIdBlocked(inviterInboxId, consentList)
if (inviterDenied) {
return 1.0
}
Expand All @@ -146,14 +186,14 @@ suspend fun computeSpamScoreGroupWelcome(appContext: Context, client: Client, gr
if (member.inboxId == inviterInboxId) {
member.addresses?.forEach { address ->
val ethereumAddress = Keys.toChecksumAddress(address)
if (client.contacts.isDenied(ethereumAddress)) {
if (isAddressBlocked(ethereumAddress, consentList)) {
return 1.0
}
}

member.addresses?.forEach { address ->
val ethereumAddress = Keys.toChecksumAddress(address)
if (client.contacts.isAllowed(ethereumAddress)) {
if (isAddressAllowed(ethereumAddress, consentList)) {
return -1.0
}
}
Expand All @@ -168,8 +208,6 @@ suspend fun computeSpamScoreGroupWelcome(appContext: Context, client: Client, gr
}
}
}


} catch (e: Exception) {
return 0.0
}
Expand Down
27 changes: 5 additions & 22 deletions android/app/src/main/java/com/converse/xmtp/Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,13 @@ import org.xmtp.android.library.codecs.AttachmentCodec
import org.xmtp.android.library.codecs.ReactionCodec
import org.xmtp.android.library.codecs.RemoteAttachmentCodec
import org.xmtp.android.library.codecs.ReplyCodec
import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder

fun initCodecs() {
Client.register(codec = AttachmentCodec())
Client.register(codec = RemoteAttachmentCodec())
Client.register(codec = ReactionCodec())
Client.register(codec = ReplyCodec())
}

fun getXmtpKeyForAccount(appContext: Context, account: String): String? {
val legacyKey = getKeychainValue("XMTP_BASE64_KEY")
if (legacyKey != null && legacyKey.isNotEmpty()) {
Log.d("XmtpClient", "Legacy Key Found: ${legacyKey} ${legacyKey.length}")
return legacyKey
}

val accountKey = getKeychainValue("XMTP_KEY_${account}")
if (accountKey != null && accountKey.isNotEmpty()) {
Log.d("XmtpClient", "Found key for account: ${account}")
return accountKey
}
return null
}

fun getDbEncryptionKey(): ByteArray? {
val key = getKeychainValue("LIBXMTP_DB_ENCRYPTION_KEY")
if (key != null) {
Expand All @@ -46,9 +29,6 @@ fun getDbEncryptionKey(): ByteArray? {
}

suspend fun getXmtpClient(appContext: Context, account: String): Client? {
val keyString = getXmtpKeyForAccount(appContext, account) ?: return null
val keyByteArray = Base64.decode(keyString)
val keys = PrivateKeyBundleV1Builder.buildFromBundle(keyByteArray)
val mmkv = getMmkv(appContext)
var xmtpEnvString = mmkv?.decodeString("xmtp-env")
// TODO => stop using async storage
Expand All @@ -60,8 +40,11 @@ suspend fun getXmtpClient(appContext: Context, account: String): Client? {

val dbDirectory = "/data/data/${appContext.packageName}/databases"
val dbEncryptionKey = getDbEncryptionKey()
if (dbEncryptionKey == null) {
throw Error("Missing dbEncryptionKey")
}

val options = ClientOptions(api = ClientOptions.Api(env = xmtpEnv, isSecure = true), enableV3 = true, dbEncryptionKey = dbEncryptionKey, dbDirectory = dbDirectory, appContext = appContext)
val options = ClientOptions(api = ClientOptions.Api(env = xmtpEnv, isSecure = true), dbEncryptionKey = dbEncryptionKey, dbDirectory = dbDirectory, appContext = appContext)

return Client().buildFrom(bundle = keys, options = options)
return Client().build(address = account, options = options)
}
Loading

0 comments on commit cffc384

Please sign in to comment.