From 7a68cf867ed082375c8c31a41d971000f0b831af Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 30 Aug 2023 11:54:36 -0300 Subject: [PATCH] block users using amber --- .../vitorpamplona/amethyst/model/Account.kt | 69 +++++++++- .../ui/dal/HiddenAccountsFeedFilter.kt | 13 +- .../ui/screen/loggedIn/AccountViewModel.kt | 5 + .../ui/screen/loggedIn/ProfileScreen.kt | 120 +++++++++++++++++- .../quartz/events/GeneralListEvent.kt | 4 + .../quartz/events/PeopleListEvent.kt | 64 ++++++++++ 6 files changed, 263 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 2758d368be..1bfac4864d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -105,12 +105,42 @@ class Account( val liveHiddenUsers: LiveData by lazy { live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent -> - val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(keyPair.privKey) - LiveHiddenUsers( - hiddenUsers = liveBlockedUsers ?: persistentSetOf(), - spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), - showSensitiveContent = showSensitiveContent - ) + val blockList = liveMuteListEvent?.note?.event as? PeopleListEvent + if (loginWithAmber) { + if (blockList?.decryptedContent == null) { + GlobalScope.launch(Dispatchers.IO) { + val content = blockList?.content ?: "" + if (content.isEmpty()) return@launch + AmberUtils.content = "" + AmberUtils.decryptBookmark( + content, + keyPair.pubKey.toHexKey() + ) + blockList?.decryptedContent = AmberUtils.content + AmberUtils.content = "" + } + + LiveHiddenUsers( + hiddenUsers = persistentSetOf(), + spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), + showSensitiveContent = showSensitiveContent + ) + } else { + val liveBlockedUsers = blockList.publicAndPrivateUsers(blockList.decryptedContent ?: "") + LiveHiddenUsers( + hiddenUsers = liveBlockedUsers, + spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), + showSensitiveContent = showSensitiveContent + ) + } + } else { + val liveBlockedUsers = blockList?.publicAndPrivateUsers(keyPair.privKey) + LiveHiddenUsers( + hiddenUsers = liveBlockedUsers ?: persistentSetOf(), + spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), + showSensitiveContent = showSensitiveContent + ) + } }.distinctUntilChanged() } @@ -1654,6 +1684,28 @@ class Account( return returningList } + fun hideUser(pubkeyHex: String, encryptedContent: String): PeopleListEvent? { + val blockList = migrateHiddenUsersIfNeeded(getBlockList()) + + return if (blockList != null) { + PeopleListEvent.addUser( + earlierVersion = blockList, + pubKeyHex = pubkeyHex, + isPrivate = true, + pubKey = keyPair.pubKey.toHexKey(), + encryptedContent = encryptedContent + ) + } else { + PeopleListEvent.createListWithUser( + name = PeopleListEvent.blockList, + pubKeyHex = pubkeyHex, + isPrivate = true, + pubKey = keyPair.pubKey.toHexKey(), + encryptedContent = encryptedContent + ) + } + } + fun hideUser(pubkeyHex: String) { val blockList = migrateHiddenUsersIfNeeded(getBlockList()) @@ -2065,7 +2117,12 @@ class Account( fun isHidden(userHex: String): Boolean { val blockList = getBlockList() + val decryptedContent = blockList?.decryptedContent ?: "" + if (loginWithAmber) { + if (decryptedContent.isBlank()) return false + return (blockList?.publicAndPrivateUsers(decryptedContent)?.contains(userHex) ?: false) || userHex in transientHiddenUsers + } return (blockList?.publicAndPrivateUsers(keyPair.privKey)?.contains(userHex) ?: false) || userHex in transientHiddenUsers } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt index 43d78eb6a9..608eb1384b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt @@ -14,7 +14,18 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { } override fun feed(): List { - return account.getBlockList() + val blockList = account.getBlockList() + val decryptedContent = blockList?.decryptedContent ?: "" + if (account.loginWithAmber) { + if (decryptedContent.isEmpty()) return emptyList() + + return blockList + ?.publicAndPrivateUsers(decryptedContent) + ?.map { LocalCache.getOrCreateUser(it) } + ?: emptyList() + } + + return blockList ?.publicAndPrivateUsers(account.keyPair.privKey) ?.map { LocalCache.getOrCreateUser(it) } ?: emptyList() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 2c13780db2..44f9025384 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse +import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.SealedGossipEvent @@ -472,6 +473,10 @@ class AccountViewModel(val account: Account) : ViewModel() { } } + fun hide(user: User, encryptedContent: String): PeopleListEvent? { + return account.hideUser(user.pubkeyHex, encryptedContent) + } + fun hide(user: User) { viewModelScope.launch(Dispatchers.IO) { account.hideUser(user.pubkeyHex) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 084e354d0d..4dc0280e91 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1,8 +1,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import android.app.Activity import android.content.Intent import android.net.Uri import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.* @@ -53,14 +56,17 @@ import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.AmberUtils import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView import com.vitorpamplona.amethyst.ui.actions.SignerDialog +import com.vitorpamplona.amethyst.ui.actions.SignerType import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus import com.vitorpamplona.amethyst.ui.components.InvoiceRequestCard @@ -95,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.BadgeDefinitionEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent @@ -625,7 +632,9 @@ private fun ProfileHeader( modifier = Modifier .size(30.dp) .align(Alignment.Center), - onClick = { popupExpanded = true }, + onClick = { + popupExpanded = true + }, shape = ButtonBorder, colors = ButtonDefaults .buttonColors( @@ -1715,6 +1724,80 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () -> ) { val clipboardManager = LocalClipboardManager.current val scope = rememberCoroutineScope() + val context = LocalContext.current + + var event by remember { mutableStateOf(null) } + if (event != null) { + SignerDialog( + onClose = { + event = null + }, + onPost = { + scope.launch(Dispatchers.IO) { + val signedEvent = Event.fromJson(it) + Client.send(signedEvent) + LocalCache.verifyAndConsume(signedEvent, null) + event = null + accountViewModel.account.live.invalidateData() + accountViewModel.account.saveable.invalidateData() + onDismiss() + } + }, + data = event!!.toJson() + ) + } + + val encryptResult = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { + if (it.resultCode != Activity.RESULT_OK) { + scope.launch { + Toast.makeText( + context, + "Sign request rejected", + Toast.LENGTH_SHORT + ).show() + } + return@rememberLauncherForActivityResult + } + + val encryptedContent = it.data?.getStringExtra("signature") ?: "" + event = accountViewModel.hide(user, encryptedContent) + } + ) + + val decryptResult = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { + if (it.resultCode != Activity.RESULT_OK) { + scope.launch { + Toast.makeText( + context, + "Sign request rejected", + Toast.LENGTH_SHORT + ).show() + } + return@rememberLauncherForActivityResult + } + + val decryptedContent = it.data?.getStringExtra("signature") ?: "" + val blockList = accountViewModel.account.getBlockList() + val privateTags = if (blockList == null) { + listOf(listOf("p", user.pubkeyHex)) + } else { + blockList.privateTagsOrEmpty(decryptedContent).plus(element = listOf("p", user.pubkeyHex)) + } + val msg = Event.mapper.writeValueAsString(privateTags) + + ServiceManager.shouldPauseService = true + AmberUtils.openAmber( + msg, + SignerType.NIP04_ENCRYPT, + encryptResult, + accountViewModel.account.keyPair.pubKey.toHexKey() + ) + } + ) DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); onDismiss() }) { Text(stringResource(R.string.copy_user_id)) @@ -1730,10 +1813,37 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () -> Text(stringResource(R.string.unblock_user)) } } else { - DropdownMenuItem(onClick = { - accountViewModel.hide(user) - onDismiss() - }) { + DropdownMenuItem( + onClick = { + if (accountViewModel.loggedInWithAmber()) { + scope.launch(Dispatchers.IO) { + val blockList = accountViewModel.account.getBlockList() + val content = blockList?.content ?: "" + if (content.isBlank()) { + val privateTags = listOf(listOf("p", user.pubkeyHex)) + val msg = Event.mapper.writeValueAsString(privateTags) + + AmberUtils.openAmber( + msg, + SignerType.NIP04_ENCRYPT, + encryptResult, + accountViewModel.account.keyPair.pubKey.toHexKey() + ) + } else { + AmberUtils.openAmber( + content, + SignerType.NIP04_DECRYPT, + decryptResult, + accountViewModel.account.keyPair.pubKey.toHexKey() + ) + } + } + } else { + accountViewModel.hide(user) + onDismiss() + } + } + ) { Text(stringResource(id = R.string.block_hide_user)) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt index 54a895a02f..9a98b586c5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt @@ -70,6 +70,10 @@ abstract class GeneralListEvent( return privateTags(privKey) ?: emptyList() } + fun privateTagsOrEmpty(content: String): List> { + return privateTags(content) ?: emptyList() + } + fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } fun privateTaggedUsers(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } fun privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt index 0e216c746e..4b047283e9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt @@ -18,6 +18,7 @@ class PeopleListEvent( content: String, sig: HexKey ) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { + var decryptedContent: String? = null var publicAndPrivateUserCache: ImmutableSet? = null fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet { @@ -35,6 +36,20 @@ class PeopleListEvent( return publicAndPrivateUserCache ?: persistentSetOf() } + fun publicAndPrivateUsers(decryptedContent: String): ImmutableSet { + publicAndPrivateUserCache?.let { + return it + } + + val privateUserList = privateTagsOrEmpty(decryptedContent).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet() + + val publicUserList = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet() + + publicAndPrivateUserCache = (privateUserList + publicUserList).toImmutableSet() + + return publicAndPrivateUserCache ?: persistentSetOf() + } + fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray): Boolean { return if (isPrivate) { privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == "p" && it[1] == idHex } @@ -43,6 +58,14 @@ class PeopleListEvent( } } + fun isTaggedUser(idHex: String, isPrivate: Boolean, content: String): Boolean { + return if (isPrivate) { + privateTagsOrEmpty(content).any { it.size > 1 && it[0] == "p" && it[1] == idHex } + } else { + isTaggedUser(idHex) + } + } + companion object { const val kind = 30000 const val blockList = "mute" @@ -65,6 +88,24 @@ class PeopleListEvent( } } + fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent { + return if (isPrivate) { + create( + content = encryptedContent, + tags = listOf(listOf("d", name)), + pubKey = pubKey, + createdAt = createdAt + ) + } else { + create( + content = "", + tags = listOf(listOf("d", name), listOf("p", pubKeyHex)), + pubKey = pubKey, + createdAt = createdAt + ) + } + } + fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { return if (isPrivate) { create( @@ -94,6 +135,24 @@ class PeopleListEvent( } } + fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent { + return if (isPrivate) { + create( + content = encryptedContent, + tags = earlierVersion.tags, + pubKey = pubKey, + createdAt = createdAt + ) + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)), + pubKey = pubKey, + createdAt = createdAt + ) + } + } + fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { if (earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion @@ -146,5 +205,10 @@ class PeopleListEvent( val sig = CryptoUtils.sign(id, privateKey) return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } + + fun create(content: String, tags: List>, pubKey: HexKey, createdAt: Long = TimeUtils.now()): PeopleListEvent { + val id = generateId(pubKey, createdAt, kind, tags, content) + return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, "") + } } }