diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/VanishEventsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/VanishEventsScreen.kt index 67ad7b894..9f24f1b25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/VanishEventsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/VanishEventsScreen.kt @@ -65,6 +65,8 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -79,7 +81,6 @@ fun VanishEventsScreen( val vanishEvents by viewModel.vanishEvents.collectAsStateWithLifecycle() val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() - val complianceResults by viewModel.complianceResults.collectAsStateWithLifecycle() LaunchedEffect(Unit) { viewModel.load() @@ -143,9 +144,8 @@ fun VanishEventsScreen( items(vanishEvents, key = { it.event.id }) { item -> VanishEventCard( item = item, - complianceResults = complianceResults, - onTestCompliance = { relayUrl, date -> - viewModel.testCompliance(relayUrl, date) + onTestCompliance = { relay -> + viewModel.testCompliance(item, relay) }, ) } @@ -160,8 +160,7 @@ fun VanishEventsScreen( @Composable private fun VanishEventCard( item: VanishEventItem, - complianceResults: Map, - onTestCompliance: (String, Long) -> Unit, + onTestCompliance: (NormalizedRelayUrl) -> Unit, ) { Card( modifier = @@ -230,14 +229,7 @@ private fun VanishEventCard( Spacer(modifier = Modifier.height(4.dp)) - item.relays.forEach { relayUrl -> - RelayComplianceRow( - relayUrl = relayUrl, - vanishDate = item.event.createdAt, - status = complianceResults["$relayUrl:${item.event.createdAt}"] ?: ComplianceStatus.UNTESTED, - onTest = { onTestCompliance(relayUrl, item.event.createdAt) }, - ) - } + RenderRelaysWithComplianceResults(item, onTestCompliance) } if (item.event.content.isNotBlank()) { @@ -256,10 +248,24 @@ private fun VanishEventCard( } } +@Composable +private fun RenderRelaysWithComplianceResults( + item: VanishEventItem, + onTestCompliance: (NormalizedRelayUrl) -> Unit, +) { + val complianceResults by item.complianceResults.collectAsStateWithLifecycle() + item.relays.forEach { relay -> + RelayComplianceRow( + relay = relay, + status = complianceResults[relay] ?: ComplianceStatus.UNTESTED, + onTest = { onTestCompliance(relay) }, + ) + } +} + @Composable private fun RelayComplianceRow( - relayUrl: String, - vanishDate: Long, + relay: NormalizedRelayUrl, status: ComplianceStatus, onTest: () -> Unit, ) { @@ -271,11 +277,7 @@ private fun RelayComplianceRow( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = - relayUrl - .removePrefix("wss://") - .removePrefix("ws://") - .removeSuffix("/"), + text = relay.displayUrl(), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f), maxLines = 1, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/VanishEventsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/VanishEventsViewModel.kt index ea11ed718..8cff76489 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/VanishEventsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/VanishEventsViewModel.kt @@ -27,18 +27,23 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.query import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent -import com.vitorpamplona.quartz.nip62RequestToVanish.tags.RelayTag import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @Stable data class VanishEventItem( val event: RequestToVanishEvent, - val relays: List, + val relays: List, val isAllRelays: Boolean, + val complianceResults: MutableStateFlow> = + MutableStateFlow( + relays.associateWith { ComplianceStatus.UNTESTED }, + ), ) enum class ComplianceStatus { @@ -58,14 +63,10 @@ class VanishEventsViewModel : ViewModel() { private val _isLoading = MutableStateFlow(false) val isLoading = _isLoading.asStateFlow() - private val _complianceResults = MutableStateFlow>(emptyMap()) - val complianceResults = _complianceResults.asStateFlow() - fun load() { viewModelScope.launch(Dispatchers.IO) { _isLoading.value = true _vanishEvents.value = emptyList() - _complianceResults.value = emptyMap() val connectedRelays = account.client.connectedRelaysFlow().value if (connectedRelays.isEmpty()) { @@ -86,7 +87,7 @@ class VanishEventsViewModel : ViewModel() { account.client.query(filters = filtersPerRelay).mapNotNull { event -> if (event is RequestToVanishEvent) { val relayTags = event.vanishFromRelays() - val isAll = relayTags.contains(RelayTag.EVERYWHERE) + val isAll = event.vanishFromAllRelays() VanishEventItem( event = event, relays = relayTags, @@ -102,35 +103,40 @@ class VanishEventsViewModel : ViewModel() { } fun testCompliance( - relayUrl: String, - vanishDate: Long, + item: VanishEventItem, + relay: NormalizedRelayUrl, ) { - val key = "$relayUrl:$vanishDate" - _complianceResults.value = _complianceResults.value + (key to ComplianceStatus.TESTING) + item.complianceResults.update { + it + (relay to ComplianceStatus.TESTING) + } viewModelScope.launch(Dispatchers.IO) { try { val foundEvent = account.client.downloadFirstEvent( - relay = relayUrl, + relay = relay, filter = Filter( - authors = listOf(account.pubKey), - until = vanishDate - 1, + authors = listOf(item.event.pubKey), + until = item.event.createdAt - 1, limit = 1, ), ) - _complianceResults.value += ( - key to - if (foundEvent != null) { - ComplianceStatus.NON_COMPLIANT - } else { - ComplianceStatus.COMPLIANT - } - ) + item.complianceResults.update { + it + ( + relay to + if (foundEvent != null) { + ComplianceStatus.NON_COMPLIANT + } else { + ComplianceStatus.COMPLIANT + } + ) + } } catch (_: Exception) { - _complianceResults.value += (key to ComplianceStatus.ERROR) + item.complianceResults.update { + it + (relay to ComplianceStatus.ERROR) + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt index 3f85e755f..0a861cc9b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt @@ -23,14 +23,16 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import androidx.sqlite.driver.bundled.BundledSQLiteDriver import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl import com.vitorpamplona.quartz.nip01Core.store.IEventStore class EventStore( dbName: String? = "events.db", - relayUrl: String? = "wss://quartz.local", + relay: NormalizedRelayUrl? = "wss://quartz.local/".normalizeRelayUrl(), val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), ) : IEventStore { - val store = SQLiteEventStore(BundledSQLiteDriver(), dbName, relayUrl, indexStrategy) + val store = SQLiteEventStore(BundledSQLiteDriver(), dbName, relay, indexStrategy) override fun insert(event: Event) = store.insertEvent(event) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishModule.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishModule.kt index c85022bd7..13ac44a00 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishModule.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishModule.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import androidx.sqlite.SQLiteConnection import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent class RightToVanishModule( @@ -100,11 +101,11 @@ class RightToVanishModule( fun insert( event: Event, - relayUrl: String?, + relay: NormalizedRelayUrl?, headerId: Long, db: SQLiteConnection, ) { - if (event is RequestToVanishEvent && event.shouldVanishFrom(relayUrl)) { + if (event is RequestToVanishEvent && event.shouldVanishFrom(relay)) { db.prepare(insertRTV).use { stmt -> stmt.bindLong(1, headerId) stmt.bindLong(2, hasher(db).hash(event.pubKey)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index 604c671c1..faa9e7cf3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.core.isEphemeral import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.utils.EventFactory @@ -40,7 +41,7 @@ import kotlinx.coroutines.withContext class SQLiteEventStore( val driver: SQLiteDriver = BundledSQLiteDriver(), val dbName: String? = "events.db", - val relayUrl: String? = null, + val relay: NormalizedRelayUrl? = null, val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), ) { companion object { @@ -182,7 +183,7 @@ class SQLiteEventStore( deletionModule.insert(event, db) expirationModule.insert(event, headerId, db) fullTextSearchModule.insert(event, headerId, db) - rightToVanishModule.insert(event, relayUrl, headerId, db) + rightToVanishModule.insert(event, relay, headerId, db) } fun insertEvent(event: Event) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt index 449e8c9fc..e374820fe 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip62RequestToVanish.tags.shouldVanishFrom import com.vitorpamplona.quartz.nip62RequestToVanish.tags.vanishFrom +import com.vitorpamplona.quartz.nip62RequestToVanish.tags.vanishFromAllRelays import com.vitorpamplona.quartz.nip62RequestToVanish.tags.vanishFromEverywhere import com.vitorpamplona.quartz.nip62RequestToVanish.tags.vanishFromRelays import com.vitorpamplona.quartz.utils.TimeUtils @@ -42,7 +43,9 @@ class RequestToVanishEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { fun vanishFromRelays() = tags.vanishFromRelays() - fun shouldVanishFrom(relayUrl: String?) = tags.shouldVanishFrom(relayUrl) + fun vanishFromAllRelays() = tags.vanishFromAllRelays() + + fun shouldVanishFrom(relay: NormalizedRelayUrl?) = tags.shouldVanishFrom(relay) companion object { const val KIND = 62 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt index dd2aee6f4..cc586d1d2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.quartz.nip62RequestToVanish.tags import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.utils.ensure class RelayTag { @@ -33,16 +35,23 @@ class RelayTag { fun notMatch(tag: Array) = !(tag.has(0) && tag[0] == TAG_NAME) + fun shouldVanishFromEverywhere(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == EVERYWHERE + fun shouldVanishFrom( tag: Array, - relayUrl: String?, - ) = tag.has(1) && tag[0] == TAG_NAME && (tag[1] == relayUrl || tag[1] == EVERYWHERE) + relay: NormalizedRelayUrl, + ) = tag.has(1) && tag[0] == TAG_NAME && (tag[1] == relay.url || tag[1] == EVERYWHERE) - fun parse(tag: Array): String? { + fun parse(tag: Array): NormalizedRelayUrl? { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - return tag[1] + + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) + + ensure(relay != null && !relay.isLocalHost()) { return null } + + return relay } fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayExt.kt index c1a2ba7b6..c4d0a4811 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayExt.kt @@ -22,7 +22,15 @@ package com.vitorpamplona.quartz.nip62RequestToVanish.tags import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl fun TagArray.vanishFromRelays() = mapNotNull(RelayTag::parse) -fun TagArray.shouldVanishFrom(relayUrl: String?) = any(RelayTag::shouldVanishFrom, relayUrl) +fun TagArray.vanishFromAllRelays() = any(RelayTag::shouldVanishFromEverywhere) + +fun TagArray.shouldVanishFrom(relay: NormalizedRelayUrl?): Boolean = + if (relay != null) { + any(RelayTag::shouldVanishFrom, relay) + } else { + any(RelayTag::shouldVanishFromEverywhere) + }