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

group preference actions #180

Merged
merged 14 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ dependencies {
implementation 'org.web3j:crypto:5.0.0'
implementation "net.java.dev.jna:jna:5.13.0@aar"
api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
api 'org.xmtp:proto-kotlin:3.40.1'
api 'org.xmtp:proto-kotlin:3.43.2'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'app.cash.turbine:turbine:0.12.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class ContactsTest {
fun testNormalizesAddresses() {
val fixtures = fixtures()
fixtures.bobClient.ensureUserContactPublished()
val bobAddressLowercased = fixtures.bobClient.address?.lowercase()
val bobContact = fixtures.aliceClient.getUserContact(peerAddress = bobAddressLowercased!!)
val bobAddressLowerCased = fixtures.bobClient.address.lowercase()
val bobContact = fixtures.aliceClient.getUserContact(peerAddress = bobAddressLowerCased)
assert(bobContact != null)
}

Expand Down Expand Up @@ -54,7 +54,7 @@ class ContactsTest {
}

@Test
fun testBlockAddress() {
fun testDenyAddress() {
val fixtures = fixtures()

val contacts = fixtures.bobClient.contacts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ import uniffi.xmtpv3.GroupPermissions

@RunWith(AndroidJUnit4::class)
class GroupTest {
lateinit var fakeApiClient: FakeApiClient
lateinit var alixWallet: PrivateKeyBuilder
lateinit var boWallet: PrivateKeyBuilder
lateinit var alix: PrivateKey
lateinit var alixClient: Client
lateinit var bo: PrivateKey
lateinit var boClient: Client
lateinit var caroWallet: PrivateKeyBuilder
lateinit var caro: PrivateKey
lateinit var caroClient: Client
lateinit var fixtures: Fixtures
private lateinit var fakeApiClient: FakeApiClient
private lateinit var alixWallet: PrivateKeyBuilder
private lateinit var boWallet: PrivateKeyBuilder
private lateinit var alix: PrivateKey
private lateinit var alixClient: Client
private lateinit var bo: PrivateKey
private lateinit var boClient: Client
private lateinit var caroWallet: PrivateKeyBuilder
private lateinit var caro: PrivateKey
private lateinit var caroClient: Client
private lateinit var fixtures: Fixtures

@Before
fun setUp() {
Expand Down Expand Up @@ -225,7 +225,7 @@ class GroupTest {

@Test
fun testCannotSendMessageToGroupMemberNotOnV3() {
var fakeApiClient = FakeApiClient()
val fakeApiClient = FakeApiClient()
val chuxAccount = PrivateKeyBuilder()
val chux: PrivateKey = chuxAccount.getPrivateKey()
val chuxClient: Client = Client().create(account = chuxAccount, apiClient = fakeApiClient)
Expand Down Expand Up @@ -342,4 +342,41 @@ class GroupTest {
assertEquals(conversation.topic, awaitItem().topic)
}
}

@Test
fun testCanAllowGroup() {
val group = boClient.conversations.newGroup(
listOf(
alix.walletAddress,
caro.walletAddress
)
)

var result = boClient.contacts.isGroupAllowed(group.id)

assert(!result)
Comment on lines +432 to +434
Copy link
Contributor

@nplasterer nplasterer Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed that this test should fail now since the default is allow on newGroup? Possibly do you need to call refreshConsentList() to get the latest results.


boClient.contacts.allowGroup(listOf(group.id))

result = boClient.contacts.isGroupAllowed(group.id)
assert(result)
}

@Test
fun testCanDenyGroup() {
val group = boClient.conversations.newGroup(
listOf(
alix.walletAddress,
caro.walletAddress
)
)
var result = boClient.contacts.isGroupAllowed(group.id)

assert(!result)

boClient.contacts.denyGroup(listOf(group.id))

result = boClient.contacts.isGroupDenied(group.id)
assert(result)
}
}
189 changes: 134 additions & 55 deletions library/src/main/java/org/xmtp/android/library/Contacts.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.xmtp.android.library

import com.google.protobuf.kotlin.toByteStringUtf8
import kotlinx.coroutines.runBlocking
import org.xmtp.android.library.messages.ContactBundle
import org.xmtp.android.library.messages.ContactBundleBuilder
Expand All @@ -14,7 +15,7 @@ import java.util.Date
enum class ConsentState {
ALLOWED,
DENIED,
UNKNOWN
UNKNOWN,
}

data class ConsentListEntry(
Expand All @@ -23,7 +24,8 @@ data class ConsentListEntry(
val consentType: ConsentState,
) {
enum class EntryType {
ADDRESS
ADDRESS,
GROUP_ID,
}

companion object {
Expand All @@ -33,6 +35,13 @@ data class ConsentListEntry(
): ConsentListEntry {
return ConsentListEntry(address, EntryType.ADDRESS, type)
}

fun groupId(
groupId: ByteArray,
type: ConsentState = ConsentState.UNKNOWN,
): ConsentListEntry {
return ConsentListEntry(String(groupId), EntryType.GROUP_ID, type)
}
}

val key: String
Expand All @@ -45,70 +54,103 @@ class ConsentList(val client: Client) {
client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes
private val privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes

private val identifier: String = uniffi.xmtpv3.generatePrivatePreferencesTopicIdentifier(
privateKey.toByteArray()
)
private val identifier: String =
uniffi.xmtpv3.generatePrivatePreferencesTopicIdentifier(
privateKey.toByteArray(),
)

@OptIn(ExperimentalUnsignedTypes::class)
suspend fun load(): ConsentList {
val envelopes = client.apiClient.envelopes(
Topic.preferenceList(identifier).description,
Pagination(direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING)
)
val envelopes =
client.apiClient.envelopes(
Topic.preferenceList(identifier).description,
Pagination(direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING),
)
val consentList = ConsentList(client)
val preferences: MutableList<PrivatePreferencesAction> = mutableListOf()
for (envelope in envelopes) {
val payload = uniffi.xmtpv3.userPreferencesDecrypt(
publicKey.toByteArray(),
privateKey.toByteArray(),
envelope.message.toByteArray()
)
val payload =
uniffi.xmtpv3.userPreferencesDecrypt(
publicKey.toByteArray(),
privateKey.toByteArray(),
envelope.message.toByteArray(),
)

preferences.add(
PrivatePreferencesAction.parseFrom(
payload.toUByteArray().toByteArray()
)
payload.toUByteArray().toByteArray(),
),
)
}

preferences.iterator().forEach { preference ->
preference.allow?.walletAddressesList?.forEach { address ->
preference.allowAddress?.walletAddressesList?.forEach { address ->
consentList.allow(address)
}
preference.block?.walletAddressesList?.forEach { address ->
preference.denyAddress?.walletAddressesList?.forEach { address ->
consentList.deny(address)
}
preference.allowGroup?.groupIdsList?.forEach { groupId ->
consentList.allowGroup(groupId.toByteArray())
}
preference.denyGroup?.groupIdsList?.forEach { groupId ->
consentList.denyGroup(groupId.toByteArray())
}
}

return consentList
}

fun publish(entry: ConsentListEntry) {
val payload = PrivatePreferencesAction.newBuilder().also {
when (entry.consentType) {
ConsentState.ALLOWED -> it.setAllow(
PrivatePreferencesAction.Allow.newBuilder().addWalletAddresses(entry.value)
)

ConsentState.DENIED -> it.setBlock(
PrivatePreferencesAction.Block.newBuilder().addWalletAddresses(entry.value)
)

ConsentState.UNKNOWN -> it.clearMessageType()
}
}.build()

val message = uniffi.xmtpv3.userPreferencesEncrypt(
publicKey.toByteArray(),
privateKey.toByteArray(),
payload.toByteArray()
)
val payload =
PrivatePreferencesAction.newBuilder().also {
when (entry.entryType) {
ConsentListEntry.EntryType.ADDRESS -> {
when (entry.consentType) {
ConsentState.ALLOWED ->
it.setAllowAddress(
PrivatePreferencesAction.AllowAddress.newBuilder().addWalletAddresses(entry.value),
)

ConsentState.DENIED ->
it.setDenyAddress(
PrivatePreferencesAction.DenyAddress.newBuilder().addWalletAddresses(entry.value),
)

ConsentState.UNKNOWN -> it.clearMessageType()
}
}
ConsentListEntry.EntryType.GROUP_ID -> {
when (entry.consentType) {
ConsentState.ALLOWED ->
it.setAllowGroup(
PrivatePreferencesAction.AllowGroup.newBuilder().addGroupIds(entry.value.toByteStringUtf8()),
)

ConsentState.DENIED ->
it.setDenyGroup(
PrivatePreferencesAction.DenyGroup.newBuilder().addGroupIds(entry.value.toByteStringUtf8()),
)

ConsentState.UNKNOWN -> it.clearMessageType()
}
}
}
}.build()

val message =
uniffi.xmtpv3.userPreferencesEncrypt(
publicKey.toByteArray(),
privateKey.toByteArray(),
payload.toByteArray(),
)

val envelope = EnvelopeBuilder.buildFromTopic(
Topic.preferenceList(identifier),
Date(),
ByteArray(message.size) { message[it].toByte() }
)
val envelope =
EnvelopeBuilder.buildFromTopic(
Topic.preferenceList(identifier),
Date(),
ByteArray(message.size) { message[it] },
)

client.publish(listOf(envelope))
}
Expand All @@ -127,19 +169,38 @@ class ConsentList(val client: Client) {
return entry
}

fun allowGroup(groupId: ByteArray): ConsentListEntry {
val entry = ConsentListEntry.groupId(groupId, ConsentState.ALLOWED)
entries[ConsentListEntry.groupId(groupId).key] = entry

return entry
}

fun denyGroup(groupId: ByteArray): ConsentListEntry {
val entry = ConsentListEntry.groupId(groupId, ConsentState.DENIED)
entries[ConsentListEntry.groupId(groupId).key] = entry

return entry
}

fun state(address: String): ConsentState {
val entry = entries[ConsentListEntry.address(address).key]

return entry?.consentType ?: ConsentState.UNKNOWN
}

fun groupState(groupId: ByteArray): ConsentState {
val entry = entries[ConsentListEntry.groupId(groupId).key]

return entry?.consentType ?: ConsentState.UNKNOWN
}
}

data class Contacts(
var client: Client,
val knownBundles: MutableMap<String, ContactBundle> = mutableMapOf(),
val hasIntroduced: MutableMap<String, Boolean> = mutableMapOf(),
) {

var consentList: ConsentList = ConsentList(client)

fun refreshConsentList(): ConsentList {
Expand All @@ -149,14 +210,6 @@ data class Contacts(
return consentList
}

fun isAllowed(address: String): Boolean {
return consentList.state(address) == ConsentState.ALLOWED
}

fun isDenied(address: String): Boolean {
return consentList.state(address) == ConsentState.DENIED
}

fun allow(addresses: List<String>) {
for (address in addresses) {
ConsentList(client).publish(consentList.allow(address))
Expand All @@ -169,11 +222,37 @@ data class Contacts(
}
}

fun has(peerAddress: String): Boolean =
knownBundles[peerAddress] != null
fun allowGroup(groupIds: List<ByteArray>) {
for (id in groupIds) {
ConsentList(client).publish(consentList.allowGroup(id))
}
}

fun denyGroup(groupIds: List<ByteArray>) {
for (id in groupIds) {
ConsentList(client).publish(consentList.denyGroup(id))
}
}

fun isAllowed(address: String): Boolean {
return consentList.state(address) == ConsentState.ALLOWED
}

fun isDenied(address: String): Boolean {
return consentList.state(address) == ConsentState.DENIED
}

fun isGroupAllowed(groupId: ByteArray): Boolean {
return consentList.groupState(groupId) == ConsentState.ALLOWED
}

fun isGroupDenied(groupId: ByteArray): Boolean {
return consentList.groupState(groupId) == ConsentState.DENIED
}

fun has(peerAddress: String): Boolean = knownBundles[peerAddress] != null

fun needsIntroduction(peerAddress: String): Boolean =
hasIntroduced[peerAddress] != true
fun needsIntroduction(peerAddress: String): Boolean = hasIntroduced[peerAddress] != true

fun find(peerAddress: String): ContactBundle? {
val knownBundle = knownBundles[peerAddress]
Expand Down
Loading
Loading