Have FilterArray.kt in persisted sync event

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

View File

@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "40dc44925e9b6000404e2cc5e781e4c2",
"identityHash": "ba8dabe94c38a3823b4803dc0a6051ca",
"entities": [
{
"tableName": "BroadcastNostrEventReceipt",
@@ -873,7 +873,7 @@
},
{
"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": [
{
"fieldPath": "id",
@@ -894,44 +894,10 @@
"notNull": true
},
{
"fieldPath": "eventIds",
"columnName": "eventIds",
"affinity": "TEXT"
},
{
"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": "filters",
"columnName": "filters",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "nostrEventId",
@@ -1340,7 +1306,7 @@
],
"setupQueries": [
"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')"
]
}
}

View File

@@ -1,11 +1,15 @@
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.KindArray
import androidx.room.TypeConverter
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlin.time.Instant
class AuxConverters {
@@ -73,4 +77,28 @@ class AuxConverters {
logger.e("Failed to convert to KindArray $value", e)
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
}
}

View File

@@ -14,6 +14,7 @@ import androidx.room.Relation
import androidx.room.Transaction
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import kotlinx.coroutines.delay
import kotlin.time.Clock
@@ -109,8 +110,12 @@ abstract class NostrDao(
logger.i("Sync Profile with PubKey: ${nostrEvent.pubKey}")
database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest(
authorPublicKeys = arrayOf(nostrEvent.pubKey),
kinds = arrayOf(0),
filters = arrayOf(
Filter(
authors = listOf(nostrEvent.pubKey),
kinds = listOf(MetadataEvent.KIND)
)
),
relayURL = synchronizationRelayURL
)
)
@@ -150,7 +155,11 @@ abstract class NostrDao(
synchronizationRelayURLs.forEach { synchronizationRelayURL ->
database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest(
eventIds = arrayOf(post.replyToId),
filters = arrayOf(
Filter(
ids = listOf(post.replyToId)
)
),
relayURL = synchronizationRelayURL
)
)
@@ -175,7 +184,11 @@ abstract class NostrDao(
synchronizationRelayURLs.forEach { synchronizationRelayURL ->
database.synchronizeNostrEventRequestDao().upsert(
SynchronizeNostrEventRequest(
eventIds = arrayOf(post.repostId),
filters = arrayOf(
Filter(
ids = listOf(post.repostId)
)
),
relayURL = synchronizationRelayURL
)
)

View File

@@ -26,6 +26,7 @@ import kotlin.time.Instant
Index("nostrEventId"),
],
)
data class Profile(
@PrimaryKey
val publicKey: String,

View File

@@ -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.TimestampedEntity
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.KindArray
import androidx.room.Entity
@@ -10,6 +11,7 @@ import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.ExperimentalUuidApi
@@ -41,14 +43,8 @@ data class SynchronizeNostrEventRequest(
val id: String = Uuid.generateV4().toHexDashString(),
val status: String = "pending",
val relayURL: String,
val eventIds: HexArray? = null,
val authorPublicKeys: HexArray? = null, // TODO: PublicKeyArray
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,
val filters: FilterArray,
override val nostrEventId: HexKey? = null,
override val unsignedNostrEventId: Long? = null,
@@ -61,17 +57,11 @@ data class SynchronizeNostrEventRequest(
other as SynchronizeNostrEventRequest
if (limit != other.limit) return false
if (unsignedNostrEventId != other.unsignedNostrEventId) return false
if (id != other.id) return false
if (status != other.status) return false
if (relayURL != other.relayURL) return false
if (!eventIds.contentEquals(other.eventIds)) 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 (!filters.contentEquals(other.filters)) return false
if (nostrEventId != other.nostrEventId) return false
if (createdAt != other.createdAt) return false
if (updatedAt != other.updatedAt) return false
@@ -80,20 +70,15 @@ data class SynchronizeNostrEventRequest(
}
override fun hashCode(): Int {
var result = limit?.hashCode() ?: 0
result = 31 * result + (unsignedNostrEventId?.hashCode() ?: 0)
var result = unsignedNostrEventId?.hashCode() ?: 0
result = 31 * result + id.hashCode()
result = 31 * result + status.hashCode()
result = 31 * result + relayURL.hashCode()
result = 31 * result + (eventIds?.contentHashCode() ?: 0)
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 + filters.contentHashCode()
result = 31 * result + (nostrEventId?.hashCode() ?: 0)
result = 31 * result + createdAt.hashCode()
result = 31 * result + updatedAt.hashCode()
return result
}
}

View File

@@ -42,4 +42,4 @@ data class LocalProfile(
)
val synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>,
)
)

View File

@@ -0,0 +1,5 @@
package ac.aux.compose.database.model.typealiases
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
typealias FilterArray = Array<Filter>

View File

@@ -58,78 +58,85 @@ class NostrEventBroadcaster(
}
scope.launch {
val webSocketSession = httpClient.webSocketSession(
urlString = localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL
)
try {
val webSocketSession = httpClient.webSocketSession(
urlString = localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL
)
broadcastingJobs[localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL] = scope.launch {
val message = OptimizedJsonMapper.toJson(
EventCmd(
event
broadcastingJobs[localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL] = scope.launch {
val message = OptimizedJsonMapper.toJson(
EventCmd(
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 ->
when(frame) {
is Frame.Text -> {
frame.readText().let { text ->
try {
logger.d("WebSocket Read Text: $text")
val result = Json.decodeFromString<JsonArray>(text)
webSocketSession.incoming.consumeAsFlow().collect { frame ->
when(frame) {
is Frame.Text -> {
frame.readText().let { text ->
try {
logger.d("WebSocket Read Text: $text")
val result = Json.decodeFromString<JsonArray>(text)
if (result.firstOrNull()?.text == "OK") {
result.getOrNull(1)?.text?.let { eventId ->
if (eventId == localBroadcastNostrEventRequest.broadcastNostrEventRequest.nostrEventId) {
if (result.firstOrNull()?.text == "OK") {
result.getOrNull(1)?.text?.let { eventId ->
if (eventId == localBroadcastNostrEventRequest.broadcastNostrEventRequest.nostrEventId) {
onBroadcastReceipt.invoke(
BroadcastNostrEventReceipt(
nostrEventId = localBroadcastNostrEventRequest.broadcastNostrEventRequest.nostrEventId,
unsignedNostrEventId = localBroadcastNostrEventRequest.broadcastNostrEventRequest.unsignedNostrEventId,
messages = result.getOrNull(3)?.text,
isAccepted = result.getOrNull(2)?.text == "true",
relayURL = localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL
onBroadcastReceipt.invoke(
BroadcastNostrEventReceipt(
nostrEventId = localBroadcastNostrEventRequest.broadcastNostrEventRequest.nostrEventId,
unsignedNostrEventId = localBroadcastNostrEventRequest.broadcastNostrEventRequest.unsignedNostrEventId,
messages = result.getOrNull(3)?.text,
isAccepted = result.getOrNull(2)?.text == "true",
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()
broadcastingJobs.remove(localBroadcastNostrEventRequest.broadcastNostrEventRequest.relayURL)?.let { job ->
job.cancel()
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")
}
}
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()
}
}
}

View File

@@ -1,5 +1,6 @@
package ac.aux.compose.ui.view.model
import ac.aux.compose.managers.SeedManager
import ac.aux.compose.repository.NostrRepository
import ac.aux.compose.ui.view.state.FeedListUIState
import androidx.compose.runtime.getValue
@@ -10,8 +11,15 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
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.IO
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
class FeedListViewModel(
@@ -28,11 +36,28 @@ class FeedListViewModel(
fun loadNostrFeed() {
viewModelScope.launch(Dispatchers.IO) {
val nostrFeed = nostrRepository.loadNostrFeed()
feedListUIState = FeedListUIState.Loaded(
nostrEvents = nostrFeed
Filter(
kinds = listOf(
TextNoteEvent.KIND,
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
)
}
}
}

View File

@@ -147,17 +147,7 @@ class NavigationViewModel(
logger.i("synchronizeNostrEventRequest: $synchronizeNostrEventRequest")
val reqCommand = ReqCmd(
subId = synchronizeNostrEventRequest.id,
filters = listOf(
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
)
)
filters = synchronizeNostrEventRequest.filters.toList()
)
scope.launch {

View File

@@ -13,6 +13,8 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.lifecycle.viewModelScope
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.entities.Entity
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
@@ -58,9 +60,13 @@ class UnqueuedProfileSynchronizationViewModel(
nostrRepository.queueSynchronizeNostrEvent(
Relays.eventPublishRelaySet.map { normalizedRelayUrl ->
SynchronizeNostrEventRequest(
authorPublicKeys = arrayOf(profilePublicKey),
filters = arrayOf(
Filter(
authors = listOf(profilePublicKey),
kinds = listOf(MetadataEvent.KIND)
)
),
unsignedNostrEventId = unsignedNostrEventId,
kinds = arrayOf(0),
relayURL = normalizedRelayUrl.url
)
}