Improves vanishing tests

This commit is contained in:
Vitor Pamplona
2026-03-27 13:59:55 -04:00
parent 1ece456f41
commit 153666b9b4
8 changed files with 88 additions and 56 deletions
@@ -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<String, ComplianceStatus>,
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,
@@ -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<String>,
val relays: List<NormalizedRelayUrl>,
val isAllRelays: Boolean,
val complianceResults: MutableStateFlow<Map<NormalizedRelayUrl, ComplianceStatus>> =
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<Map<String, ComplianceStatus>>(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)
}
}
}
}
@@ -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)
@@ -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))
@@ -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) {
@@ -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
@@ -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<String>) = !(tag.has(0) && tag[0] == TAG_NAME)
fun shouldVanishFromEverywhere(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == EVERYWHERE
fun shouldVanishFrom(
tag: Array<String>,
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>): String? {
fun parse(tag: Array<String>): 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)
@@ -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)
}