Have FilterArray.kt in persisted sync event

This commit is contained in:
Kgothatso Ngako committed 2026-03-31 13:44:45 +02:00
1 parent 4e23206537
commit 2beed5e18a
11 files changed
+165 -139

No files matched your search

@@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "40dc44925e9b6000404e2cc5e781e4c2", "identityHash": "ba8dabe94c38a3823b4803dc0a6051ca",
"entities": [ "entities": [
{ {
"tableName": "BroadcastNostrEventReceipt", "tableName": "BroadcastNostrEventReceipt",
@@ -873,7 +873,7 @@
}, },
{ {
"tableName": "SynchronizeNostrEventRequest", "tableName": "SynchronizeNostrEventRequest",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `status` TEXT NOT NULL, `relayURL` TEXT NOT NULL, `eventIds` TEXT, `authorPublicKeys` TEXT, `kinds` TEXT, `tagName` TEXT, `since` INTEGER, `until` INTEGER, `limit` INTEGER, `search` TEXT, `nostrEventId` TEXT, `unsignedNostrEventId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`unsignedNostrEventId`) REFERENCES `UnsignedNostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `status` TEXT NOT NULL, `relayURL` TEXT NOT NULL, `filters` TEXT NOT NULL, `nostrEventId` TEXT, `unsignedNostrEventId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`unsignedNostrEventId`) REFERENCES `UnsignedNostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@@ -894,44 +894,10 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "eventIds", "fieldPath": "filters",
"columnName": "eventIds", "columnName": "filters",
"affinity": "TEXT" "affinity": "TEXT",
}, "notNull": true
{
"fieldPath": "authorPublicKeys",
"columnName": "authorPublicKeys",
"affinity": "TEXT"
},
{
"fieldPath": "kinds",
"columnName": "kinds",
"affinity": "TEXT"
},
{
"fieldPath": "tagName",
"columnName": "tagName",
"affinity": "TEXT"
},
{
"fieldPath": "since",
"columnName": "since",
"affinity": "INTEGER"
},
{
"fieldPath": "until",
"columnName": "until",
"affinity": "INTEGER"
},
{
"fieldPath": "limit",
"columnName": "limit",
"affinity": "INTEGER"
},
{
"fieldPath": "search",
"columnName": "search",
"affinity": "TEXT"
}, },
{ {
"fieldPath": "nostrEventId", "fieldPath": "nostrEventId",
@@ -1340,7 +1306,7 @@
], ],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '40dc44925e9b6000404e2cc5e781e4c2')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ba8dabe94c38a3823b4803dc0a6051ca')"
] ]
} }
} }
@@ -1,11 +1,15 @@
package ac.aux.compose.database.converters package ac.aux.compose.database.converters
import ac.aux.compose.database.model.typealiases.FilterArray
import ac.aux.compose.database.model.typealiases.HexArray import ac.aux.compose.database.model.typealiases.HexArray
import ac.aux.compose.database.model.typealiases.KindArray import ac.aux.compose.database.model.typealiases.KindArray
import androidx.room.TypeConverter import androidx.room.TypeConverter
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlin.time.Instant import kotlin.time.Instant
class AuxConverters { class AuxConverters {
@@ -73,4 +77,28 @@ class AuxConverters {
logger.e("Failed to convert to KindArray $value", e) logger.e("Failed to convert to KindArray $value", e)
return null return null
} }
@TypeConverter
fun fromFilterArray(value: FilterArray?): String? = try {
return value?.let { filterArray ->
Json.encodeToString(
filterArray.map { filter ->
OptimizedJsonMapper.toJson(filter)
}
)
}
} catch (e: Throwable) {
logger.e("Failed to convert from FilterArray $value", e)
return null
}
@TypeConverter
fun toFilterArray(value: String?): FilterArray? = try {
return value?.let {
Json.decodeFromString<FilterArray>(it)
}
} catch (e: Throwable) {
logger.e("Failed to convert to FilterArray $value", e)
return null
}
} }
@@ -14,6 +14,7 @@ import androidx.room.Relation
import androidx.room.Transaction import androidx.room.Transaction
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.time.Clock import kotlin.time.Clock
@@ -109,8 +110,12 @@ abstract class NostrDao(
logger.i("Sync Profile with PubKey: ${nostrEvent.pubKey}") logger.i("Sync Profile with PubKey: ${nostrEvent.pubKey}")
database.synchronizeNostrEventRequestDao().upsert( database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
authorPublicKeys = arrayOf(nostrEvent.pubKey), filters = arrayOf(
kinds = arrayOf(0), Filter(
authors = listOf(nostrEvent.pubKey),
kinds = listOf(MetadataEvent.KIND)
)
),
relayURL = synchronizationRelayURL relayURL = synchronizationRelayURL
) )
) )
@@ -150,7 +155,11 @@ abstract class NostrDao(
synchronizationRelayURLs.forEach { synchronizationRelayURL -> synchronizationRelayURLs.forEach { synchronizationRelayURL ->
database.synchronizeNostrEventRequestDao().upsert( database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
eventIds = arrayOf(post.replyToId), filters = arrayOf(
Filter(
ids = listOf(post.replyToId)
)
),
relayURL = synchronizationRelayURL relayURL = synchronizationRelayURL
) )
) )
@@ -175,7 +184,11 @@ abstract class NostrDao(
synchronizationRelayURLs.forEach { synchronizationRelayURL -> synchronizationRelayURLs.forEach { synchronizationRelayURL ->
database.synchronizeNostrEventRequestDao().upsert( database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
eventIds = arrayOf(post.repostId), filters = arrayOf(
Filter(
ids = listOf(post.repostId)
)
),
relayURL = synchronizationRelayURL relayURL = synchronizationRelayURL
) )
) )
@@ -26,6 +26,7 @@ import kotlin.time.Instant
Index("nostrEventId"), Index("nostrEventId"),
], ],
) )
data class Profile( data class Profile(
@PrimaryKey @PrimaryKey
val publicKey: String, val publicKey: String,
@@ -3,6 +3,7 @@ package ac.aux.compose.database.model
import ac.aux.compose.database.model.traits.OptionalNostrEventEntity import ac.aux.compose.database.model.traits.OptionalNostrEventEntity
import ac.aux.compose.database.model.traits.TimestampedEntity import ac.aux.compose.database.model.traits.TimestampedEntity
import ac.aux.compose.database.model.traits.UnsignedNostrEventEntity import ac.aux.compose.database.model.traits.UnsignedNostrEventEntity
import ac.aux.compose.database.model.typealiases.FilterArray
import ac.aux.compose.database.model.typealiases.HexArray import ac.aux.compose.database.model.typealiases.HexArray
import ac.aux.compose.database.model.typealiases.KindArray import ac.aux.compose.database.model.typealiases.KindArray
import androidx.room.Entity import androidx.room.Entity
@@ -10,6 +11,7 @@ import androidx.room.ForeignKey
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.ExperimentalUuidApi
@@ -41,14 +43,8 @@ data class SynchronizeNostrEventRequest(
val id: String = Uuid.generateV4().toHexDashString(), val id: String = Uuid.generateV4().toHexDashString(),
val status: String = "pending", val status: String = "pending",
val relayURL: String, val relayURL: String,
val eventIds: HexArray? = null,
val authorPublicKeys: HexArray? = null, // TODO: PublicKeyArray val filters: FilterArray,
val kinds: KindArray? = null, // TODO: KindArray
val tagName: String? = null,
val since: Instant? = null,
val until: Instant? = null,
val limit: Int? = null,
val search: String? = null,
override val nostrEventId: HexKey? = null, override val nostrEventId: HexKey? = null,
override val unsignedNostrEventId: Long? = null, override val unsignedNostrEventId: Long? = null,
@@ -61,17 +57,11 @@ data class SynchronizeNostrEventRequest(
other as SynchronizeNostrEventRequest other as SynchronizeNostrEventRequest
if (limit != other.limit) return false
if (unsignedNostrEventId != other.unsignedNostrEventId) return false if (unsignedNostrEventId != other.unsignedNostrEventId) return false
if (id != other.id) return false if (id != other.id) return false
if (status != other.status) return false if (status != other.status) return false
if (relayURL != other.relayURL) return false if (relayURL != other.relayURL) return false
if (!eventIds.contentEquals(other.eventIds)) return false if (!filters.contentEquals(other.filters)) return false
if (!authorPublicKeys.contentEquals(other.authorPublicKeys)) return false
if (!kinds.contentEquals(other.kinds)) return false
if (tagName != other.tagName) return false
if (since != other.since) return false
if (until != other.until) return false
if (nostrEventId != other.nostrEventId) return false if (nostrEventId != other.nostrEventId) return false
if (createdAt != other.createdAt) return false if (createdAt != other.createdAt) return false
if (updatedAt != other.updatedAt) return false if (updatedAt != other.updatedAt) return false
@@ -80,20 +70,15 @@ data class SynchronizeNostrEventRequest(
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = limit?.hashCode() ?: 0 var result = unsignedNostrEventId?.hashCode() ?: 0
result = 31 * result + (unsignedNostrEventId?.hashCode() ?: 0)
result = 31 * result + id.hashCode() result = 31 * result + id.hashCode()
result = 31 * result + status.hashCode() result = 31 * result + status.hashCode()
result = 31 * result + relayURL.hashCode() result = 31 * result + relayURL.hashCode()
result = 31 * result + (eventIds?.contentHashCode() ?: 0) result = 31 * result + filters.contentHashCode()
result = 31 * result + (authorPublicKeys?.contentHashCode() ?: 0)
result = 31 * result + (kinds?.contentHashCode() ?: 0)
result = 31 * result + (tagName?.hashCode() ?: 0)
result = 31 * result + (since?.hashCode() ?: 0)
result = 31 * result + (until?.hashCode() ?: 0)
result = 31 * result + (nostrEventId?.hashCode() ?: 0) result = 31 * result + (nostrEventId?.hashCode() ?: 0)
result = 31 * result + createdAt.hashCode() result = 31 * result + createdAt.hashCode()
result = 31 * result + updatedAt.hashCode() result = 31 * result + updatedAt.hashCode()
return result return result
} }
} }
@@ -42,4 +42,4 @@ data class LocalProfile(
) )
val synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>, val synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>,
) )
@@ -0,0 +1,5 @@
package ac.aux.compose.database.model.typealiases
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
typealias FilterArray = Array<Filter>
@@ -58,78 +58,85 @@ class NostrEventBroadcaster(
} }
scope.launch { scope.launch {
val webSocketSession = httpClient.webSocketSession( try {
urlString = localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL val webSocketSession = httpClient.webSocketSession(
) urlString = localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL
)
broadcastingJobs[localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL] = scope.launch { broadcastingJobs[localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL] = scope.launch {
val message = OptimizedJsonMapper.toJson( val message = OptimizedJsonMapper.toJson(
EventCmd( EventCmd(
event event
)
)
logger.d("Broadcasting (${localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL}): $message")
webSocketSession.send(
frame = Frame.Text(message)
) )
)
logger.d("Broadcasting (${localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL}): $message")
webSocketSession.send(
frame = Frame.Text(message)
)
onBroadcastRequestProcessed.invoke(localBroadcastNostrEventRequest.broadcastNostrEventRequest) onBroadcastRequestProcessed.invoke(localBroadcastNostrEventRequest.broadcastNostrEventRequest)
webSocketSession.incoming.consumeAsFlow().collect { frame -> webSocketSession.incoming.consumeAsFlow().collect { frame ->
when(frame) { when(frame) {
is Frame.Text -> { is Frame.Text -> {
frame.readText().let { text -> frame.readText().let { text ->
try { try {
logger.d("WebSocket Read Text: $text") logger.d("WebSocket Read Text: $text")
val result = Json.decodeFromString<JsonArray>(text) val result = Json.decodeFromString<JsonArray>(text)
if (result.firstOrNull()?.text == "OK") { if (result.firstOrNull()?.text == "OK") {
result.getOrNull(1)?.text?.let { eventId -> result.getOrNull(1)?.text?.let { eventId ->
if (eventId == localBroadcastNostrEventRequest.broadcastNostrEventRequest.nostrEventId) { if (eventId == localBroadcastNostrEventRequest.broadcastNostrEventRequest.nostrEventId) {
onBroadcastReceipt.invoke( onBroadcastReceipt.invoke(
BroadcastNostrEventReceipt( BroadcastNostrEventReceipt(
nostrEventId = localBroadcastNostrEventRequest.broadcastNostrEventRequest.nostrEventId, nostrEventId = localBroadcastNostrEventRequest.broadcastNostrEventRequest.nostrEventId,
unsignedNostrEventId = localBroadcastNostrEventRequest.broadcastNostrEventRequest.unsignedNostrEventId, unsignedNostrEventId = localBroadcastNostrEventRequest.broadcastNostrEventRequest.unsignedNostrEventId,
messages = result.getOrNull(3)?.text, messages = result.getOrNull(3)?.text,
isAccepted = result.getOrNull(2)?.text == "true", isAccepted = result.getOrNull(2)?.text == "true",
relayURL = localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL relayURL = localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL
)
) )
) }
} }
} }
} catch (e: Throwable) {
logger.e("Error processing receipt: $text", e)
} }
} catch (e: Throwable) { }
logger.e("Error processing receipt: $text", e) webSocketSession.close()
broadcastingJobs.remove(localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL)?.let { job ->
job.cancel()
} }
} }
webSocketSession.close() is Frame.Close -> {
broadcastingJobs.remove(localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL)?.let { job -> logger.d("Close $frame")
job.cancel() }
is Frame.Ping -> {
logger.d("Ping: $frame")
}
is Frame.Pong -> {
logger.d("Pong: $frame")
}
is Frame.Binary -> {
logger.d("Binary Message: $frame")
}
else -> {
logger.d("Unsupported frame: $frame")
} }
} }
is Frame.Close -> {
logger.d("Close $frame")
}
is Frame.Ping -> {
logger.d("Ping: $frame")
}
is Frame.Pong -> {
logger.d("Pong: $frame")
}
is Frame.Binary -> {
logger.d("Binary Message: $frame")
}
else -> {
logger.d("Unsupported frame: $frame")
}
} }
logger.i("Websocket exit")
} }
logger.i("Websocket exit") broadcastingJobs[localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL]?.join()
} catch (e: Throwable) {
logger.e("Error broadcasting event: ", e)
// TODO: Save this a failure
onBroadcastRequestProcessed.invoke(localBroadcastNostrEventRequest.broadcastNostrEventRequest)
} }
broadcastingJobs[localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL]?.join()
} }
} }
} }
@@ -1,5 +1,6 @@
package ac.aux.compose.ui.view.model package ac.aux.compose.ui.view.model
import ac.aux.compose.managers.SeedManager
import ac.aux.compose.repository.NostrRepository import ac.aux.compose.repository.NostrRepository
import ac.aux.compose.ui.view.state.FeedListUIState import ac.aux.compose.ui.view.state.FeedListUIState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -10,8 +11,15 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class FeedListViewModel( class FeedListViewModel(
@@ -28,11 +36,28 @@ class FeedListViewModel(
fun loadNostrFeed() { fun loadNostrFeed() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val nostrFeed = nostrRepository.loadNostrFeed() Filter(
kinds = listOf(
feedListUIState = FeedListUIState.Loaded( TextNoteEvent.KIND,
nostrEvents = nostrFeed RepostEvent.KIND,
ReactionEvent.KIND,
LnZapEvent.KIND,
),
tags = mapOf(
Pair("#p", listOf(SeedManager.activePublicKey().toHexKey()))
)
) )
val localProfile = nostrRepository.observeProfile(SeedManager.activePublicKey().toHexKey()).firstOrNull()
if (localProfile?.profile == null) {
feedListUIState = FeedListUIState.Error
} else {
val nostrFeed = nostrRepository.loadNostrFeed()
feedListUIState = FeedListUIState.Loaded(
nostrEvents = nostrFeed
)
}
} }
} }
@@ -147,17 +147,7 @@ class NavigationViewModel(
logger.i("synchronizeNostrEventRequest: $synchronizeNostrEventRequest") logger.i("synchronizeNostrEventRequest: $synchronizeNostrEventRequest")
val reqCommand = ReqCmd( val reqCommand = ReqCmd(
subId = synchronizeNostrEventRequest.id, subId = synchronizeNostrEventRequest.id,
filters = listOf( filters = synchronizeNostrEventRequest.filters.toList()
Filter(
ids = synchronizeNostrEventRequest.eventIds?.map { eventId -> eventId },
authors = synchronizeNostrEventRequest.authorPublicKeys?.map { authorPublicKey -> authorPublicKey },
kinds = synchronizeNostrEventRequest.kinds?.map { kind -> kind },
since = synchronizeNostrEventRequest.since?.epochSeconds,
until = synchronizeNostrEventRequest.until?.epochSeconds,
limit = synchronizeNostrEventRequest.limit,
search = synchronizeNostrEventRequest.search
)
)
) )
scope.launch { scope.launch {
@@ -13,6 +13,8 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
@@ -58,9 +60,13 @@ class UnqueuedProfileSynchronizationViewModel(
nostrRepository.queueSynchronizeNostrEvent( nostrRepository.queueSynchronizeNostrEvent(
Relays.eventPublishRelaySet.map { normalizedRelayUrl -> Relays.eventPublishRelaySet.map { normalizedRelayUrl ->
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
authorPublicKeys = arrayOf(profilePublicKey), filters = arrayOf(
Filter(
authors = listOf(profilePublicKey),
kinds = listOf(MetadataEvent.KIND)
)
),
unsignedNostrEventId = unsignedNostrEventId, unsignedNostrEventId = unsignedNostrEventId,
kinds = arrayOf(0),
relayURL = normalizedRelayUrl.url relayURL = normalizedRelayUrl.url
) )
} }