From 63c2530075420b3dc85e29d4483da2fdbffef6b6 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 30 Mar 2026 01:17:44 +0200 Subject: [PATCH] Bug fix on infinite sync --- .../1.json | 11 +- .../ac/aux/compose/database/AuxDatabase.kt | 4 +- .../compose/database/converters/Converters.kt | 48 ++++- .../ac/aux/compose/database/dao/NostrDao.kt | 58 +++--- .../model/SynchronizeNostrEventRequest.kt | 56 +++++- .../database/model/typealiases/HexArray.kt | 4 + .../database/model/typealiases/KindArray.kt | 3 + .../repository/DatabaseNostrRepository.kt | 28 +-- .../UnqueuedProfileSynchronizationScreen.kt | 3 + .../ui/composable/navigation/AuxNavHost.kt | 4 +- .../UnqueuedProfileSynchronizationRoute.kt | 1 + .../ui/view/model/NavigationViewModel.kt | 188 +++++++++--------- ...UnqueuedProfileSynchronizationViewModel.kt | 11 +- 13 files changed, 272 insertions(+), 147 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/typealiases/HexArray.kt create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/typealiases/KindArray.kt diff --git a/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json b/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json index 6b4acd40..f46bdc03 100644 --- a/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json +++ b/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "75e5c80b2c3183c8a4fa300627ffb5db", + "identityHash": "9a3031f0a4cb56c9b5d6c26be0f48a3c", "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, `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, `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 )", "fields": [ { "fieldPath": "id", @@ -928,6 +928,11 @@ "columnName": "limit", "affinity": "INTEGER" }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT" + }, { "fieldPath": "nostrEventId", "columnName": "nostrEventId", @@ -1330,7 +1335,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, '75e5c80b2c3183c8a4fa300627ffb5db')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a3031f0a4cb56c9b5d6c26be0f48a3c')" ] } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt index 236c13fe..4cb61076 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt @@ -1,6 +1,6 @@ package ac.aux.compose.database -import ac.aux.compose.database.converters.Converters +import ac.aux.compose.database.converters.AuxConverters import ac.aux.compose.database.dao.BroadcastNostrEventReceiptDao import ac.aux.compose.database.dao.BroadcastNostrEventRequestDao import ac.aux.compose.database.dao.NostrDao @@ -46,7 +46,7 @@ import androidx.room.useWriterConnection ], version = 1 ) -@TypeConverters(Converters::class) +@TypeConverters(AuxConverters::class) abstract class AuxDatabase: RoomDatabase() { abstract fun broadcastNostrEventReceiptDao(): BroadcastNostrEventReceiptDao abstract fun broadcastNostrEventRequestDao(): BroadcastNostrEventRequestDao diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/converters/Converters.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/converters/Converters.kt index 083d10cb..87ebf447 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/converters/Converters.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/converters/Converters.kt @@ -1,11 +1,16 @@ package ac.aux.compose.database.converters +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.TagArray import kotlinx.serialization.json.Json import kotlin.time.Instant -class Converters { +class AuxConverters { + val logger = Logger.withTag("AuxConverters") + @TypeConverter fun fromTimestamp(value: Long?): Instant? { return value?.let { Instant.fromEpochMilliseconds(it) } @@ -27,4 +32,45 @@ class Converters { // Decodes the JSON string back into the nested array structure return value?.let { Json.decodeFromString(it) } } + + + @TypeConverter + fun fromHexArray(value: HexArray?): String? = try { + return value?.let { + Json.encodeToString(it).lowercase() + } + } catch (e: Throwable) { + logger.e("Failed to convert from HexArray $value", e) + return null + } + + @TypeConverter + fun toHexArray(value: String?): HexArray? = try { + return value?.let { + Json.decodeFromString(it) + } + } catch (e: Throwable) { + logger.e("Failed to convert to HexArray $value", e) + return null + } + + @TypeConverter + fun fromKindArray(value: KindArray?): String? = try { + return value?.let { + Json.encodeToString(it) + } + } catch (e: Throwable) { + logger.e("Failed to convert from KindArray $value", e) + return null + } + + @TypeConverter + fun toKindArray(value: String?): KindArray? = try { + return value?.let { + Json.decodeFromString(it) + } + } catch (e: Throwable) { + logger.e("Failed to convert to KindArray $value", e) + return null + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt index a1257545..530be560 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt @@ -83,36 +83,38 @@ abstract class NostrDao( synchronizationRelayURLs: List ) { logger.i("Store Nostr Event: $nostrEvent") - // Find or create profile with the pubKey... if not found submit a sync request... - val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey) + if (nostrEvent.unsignedNostrEventId == null) { + // Find or create profile with the pubKey... if not found submit a sync request... + val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey) - if (profile == null) { - val placeHolderProfile = Profile( - publicKey = nostrEvent.pubKey, - nostrEventId = nostrEvent.pubKey // This is illegal... because the nostrEventId should be the one with all the profile information - ) - val placeHolderProfileNostrEvent = NostrEvent( - id = placeHolderProfile.publicKey, - kind = 30024, - pubKey = placeHolderProfile.publicKey, - content = placeHolderProfile.publicKey, - createdAt = Clock.System.now(), - sig = placeHolderProfile.publicKey, - tags = emptyArray(), - ) - database.nostrEventDao().upsert(placeHolderProfileNostrEvent) - database.profileDao().upsert(placeHolderProfile) - - synchronizationRelayURLs.forEach { synchronizationRelayURL -> - database.synchronizeNostrEventRequestDao().upsert( - SynchronizeNostrEventRequest( - authorPublicKeys = listOf(nostrEvent.pubKey).joinToString(","), - relayURL = synchronizationRelayURL - ) + if (profile == null) { + val placeHolderProfile = Profile( + publicKey = nostrEvent.pubKey, + nostrEventId = nostrEvent.pubKey // This is illegal... because the nostrEventId should be the one with all the profile information ) + val placeHolderProfileNostrEvent = NostrEvent( + id = placeHolderProfile.publicKey, + kind = 30024, + pubKey = placeHolderProfile.publicKey, + content = placeHolderProfile.publicKey, + createdAt = Clock.System.now(), + sig = placeHolderProfile.publicKey, + tags = emptyArray(), + ) + database.nostrEventDao().upsert(placeHolderProfileNostrEvent) + database.profileDao().upsert(placeHolderProfile) + + synchronizationRelayURLs.forEach { synchronizationRelayURL -> + database.synchronizeNostrEventRequestDao().upsert( + SynchronizeNostrEventRequest( + authorPublicKeys = arrayOf(nostrEvent.pubKey), + kinds = arrayOf(0), + relayURL = synchronizationRelayURL + ) + ) + } } } - database.nostrEventDao().upsert(nostrEvent) // Index nostrEvent @@ -146,7 +148,7 @@ abstract class NostrDao( synchronizationRelayURLs.forEach { synchronizationRelayURL -> database.synchronizeNostrEventRequestDao().upsert( SynchronizeNostrEventRequest( - eventIds = listOf(post.replyToId).joinToString(","), + eventIds = arrayOf(post.replyToId), relayURL = synchronizationRelayURL ) ) @@ -171,7 +173,7 @@ abstract class NostrDao( synchronizationRelayURLs.forEach { synchronizationRelayURL -> database.synchronizeNostrEventRequestDao().upsert( SynchronizeNostrEventRequest( - eventIds = listOf(post.repostId).joinToString(","), + eventIds = arrayOf(post.repostId), relayURL = synchronizationRelayURL ) ) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/SynchronizeNostrEventRequest.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/SynchronizeNostrEventRequest.kt index 959139d0..60745f77 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/SynchronizeNostrEventRequest.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/SynchronizeNostrEventRequest.kt @@ -1,9 +1,10 @@ package ac.aux.compose.database.model -import ac.aux.compose.database.model.traits.NostrEventEntity 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.HexArray +import ac.aux.compose.database.model.typealiases.KindArray import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index @@ -40,16 +41,59 @@ data class SynchronizeNostrEventRequest( val id: String = Uuid.generateV4().toHexDashString(), val status: String = "pending", val relayURL: String, - val eventIds: String? = null, // TODO: EventIdArray - val authorPublicKeys: String? = null, // TODO: PublicKeyArray - val kinds: String? = null, // TODO: KindArray + 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: Long? = null, + val limit: Int? = null, + val search: String? = null, override val nostrEventId: HexKey? = null, override val unsignedNostrEventId: Long? = null, override val createdAt: Instant = Clock.System.now(), override val updatedAt: Instant = createdAt, -): OptionalNostrEventEntity, UnsignedNostrEventEntity, TimestampedEntity \ No newline at end of file +): OptionalNostrEventEntity, UnsignedNostrEventEntity, TimestampedEntity { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + 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 (nostrEventId != other.nostrEventId) return false + if (createdAt != other.createdAt) return false + if (updatedAt != other.updatedAt) return false + + return true + } + + override fun hashCode(): Int { + var result = limit?.hashCode() ?: 0 + result = 31 * 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 + (nostrEventId?.hashCode() ?: 0) + result = 31 * result + createdAt.hashCode() + result = 31 * result + updatedAt.hashCode() + return result + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/typealiases/HexArray.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/typealiases/HexArray.kt new file mode 100644 index 00000000..fb08f2d7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/typealiases/HexArray.kt @@ -0,0 +1,4 @@ +package ac.aux.compose.database.model.typealiases + +typealias HexArray = Array + diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/typealiases/KindArray.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/typealiases/KindArray.kt new file mode 100644 index 00000000..4bcfa0b4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/typealiases/KindArray.kt @@ -0,0 +1,3 @@ +package ac.aux.compose.database.model.typealiases + +typealias KindArray = Array \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt index bd621614..4e640ed1 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt @@ -81,18 +81,19 @@ class DatabaseNostrRepository( // Look busy for the onboarding screen delay(3_500) - if (unsignedNostrEvent.signedAt != null) { - // Submit a SyncNostrEventRequest - synchronizationRelayURLs.forEach { relayUrl -> - database.synchronizeNostrEventRequestDao().upsert( - SynchronizeNostrEventRequest( - authorPublicKeys = unsignedNostrEvent.pubKey, - status = "pending", - relayURL = relayUrl - ) - ) - } - } +// if (unsignedNostrEvent.signedAt != null) { +// // Submit a SyncNostrEventRequest +// synchronizationRelayURLs.forEach { relayUrl -> +// database.synchronizeNostrEventRequestDao().upsert( +// SynchronizeNostrEventRequest( +// authorPublicKeys = arrayOf(unsignedNostrEvent.pubKey), +// kinds = arrayOf(0), +// status = "pending", +// relayURL = relayUrl +// ) +// ) +// } +// } } } @@ -189,7 +190,8 @@ class DatabaseNostrRepository( database.synchronizeNostrEventRequestDao().upsert( synchronizeNostrEventRequest.copy( - nostrEventId = nostrEvent.id + nostrEventId = nostrEvent.id, + status = "processed" ) ) } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/UnqueuedProfileSynchronizationScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/UnqueuedProfileSynchronizationScreen.kt index f30ff747..33df6cc8 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/UnqueuedProfileSynchronizationScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/UnqueuedProfileSynchronizationScreen.kt @@ -28,11 +28,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun UnqueuedProfileSynchronizationScreen( + unsignedNostrEventId: Long, profilePublicKey: String, nostrRepository: NostrRepository ) { val unqueuedProfileSynchronizationViewModel: UnqueuedProfileSynchronizationViewModel = viewModel( factory = UnqueuedProfileSynchronizationViewModel.factory( + unsignedNostrEventId = unsignedNostrEventId, profilePublicKey = profilePublicKey, nostrRepository = nostrRepository ) @@ -90,6 +92,7 @@ private fun UnqueuedProfileSynchronizationScreenPreview() { modifier = Modifier.fillMaxSize() ) { UnqueuedProfileSynchronizationScreen( + unsignedNostrEventId = 2, profilePublicKey = "npub is for living.", nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY ) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt index 2581318d..d1ebf684 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt @@ -132,7 +132,8 @@ fun AuxNavHost( is NavigationUIState.UnqueuedProfileSynchronization -> { navController.navigate( route = UnqueuedProfileSynchronizationRoute( - state.unsignedNostrEvent.pubKey + unsignedNostrEventId = state.unsignedNostrEvent.id, + publicKey = state.unsignedNostrEvent.pubKey ) ) { popUpTo(0) @@ -255,6 +256,7 @@ fun AuxNavHost( composable { backStackEntry -> val route = backStackEntry.toRoute() UnqueuedProfileSynchronizationScreen( + unsignedNostrEventId = route.unsignedNostrEventId, profilePublicKey = route.publicKey, nostrRepository = nostrRepository ) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/UnqueuedProfileSynchronizationRoute.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/UnqueuedProfileSynchronizationRoute.kt index fa10946f..528bd0e1 100755 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/UnqueuedProfileSynchronizationRoute.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/UnqueuedProfileSynchronizationRoute.kt @@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable @Serializable data class UnqueuedProfileSynchronizationRoute( val publicKey: String, + val unsignedNostrEventId: Long, ): Route() { } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NavigationViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NavigationViewModel.kt index 7a08aa19..7f996db4 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NavigationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NavigationViewModel.kt @@ -12,7 +12,10 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.utils.text import io.ktor.client.HttpClient @@ -34,9 +37,7 @@ import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject import kotlin.collections.set import kotlin.time.Instant @@ -91,7 +92,7 @@ class NavigationViewModel( observeUnsignedNostrEvents() observePendingBroadcastNostrEventRequests() observeProfile() - observeSyncNostrEventRequests() + observePendingSyncNostrEventRequests() } @@ -133,118 +134,123 @@ class NavigationViewModel( } } - private fun observeSyncNostrEventRequests() { - logger.i { "observeSyncNostrEventRequests" } + private fun observePendingSyncNostrEventRequests() { + logger.i { "observePendingSyncNostrEventRequests" } scope.launch(Dispatchers.IO) { nostrRepository.observePendingSynchronizeNostrEventRequests().collect { synchronizeNostrEventRequestOrNull -> synchronizeNostrEventRequestOrNull?.let { synchronizeNostrEventRequest -> - val filterContent = mutableMapOf() - - synchronizeNostrEventRequest.eventIds?.let { eventIds -> - filterContent.put("ids", JsonPrimitive(eventIds)) - } - synchronizeNostrEventRequest.authorPublicKeys?.let { authorPublicKeys -> - filterContent.put("authors", JsonPrimitive(authorPublicKeys)) - } - synchronizeNostrEventRequest.kinds?.let { kinds -> - filterContent.put("kinds", JsonPrimitive(kinds)) - } - synchronizeNostrEventRequest.since?.let { since -> - filterContent.put("since", JsonPrimitive(since.toEpochMilliseconds())) - } - synchronizeNostrEventRequest.until?.let { until -> - filterContent.put("until", JsonPrimitive(until.toEpochMilliseconds())) - } - synchronizeNostrEventRequest.limit?.let { limit -> - filterContent.put("limit", JsonPrimitive(limit)) - } - - val filter = JsonObject( - filterContent - ) - logger.d("Syncing: $filter") - scope.launch { - val webSocketSession = httpClient.webSocketSession( - urlString = synchronizeNostrEventRequest.relayURL + try { + 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?.toEpochMilliseconds(), + until = synchronizeNostrEventRequest.until?.toEpochMilliseconds(), + limit = synchronizeNostrEventRequest.limit, + search = synchronizeNostrEventRequest.search + ) + ) ) - syncingJobs[synchronizeNostrEventRequest.relayURL] = scope.launch { - webSocketSession.send( - frame = Frame.Text("[\"REQ\",${synchronizeNostrEventRequest.id},${filter}]") + scope.launch { + val webSocketSession = httpClient.webSocketSession( + urlString = synchronizeNostrEventRequest.relayURL ) - nostrRepository.synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest) + syncingJobs[synchronizeNostrEventRequest.relayURL] = scope.launch { + val filterRequest = OptimizedJsonMapper.toJson(reqCommand) + logger.d("Syncing: $filterRequest") + webSocketSession.send( + frame = Frame.Text(filterRequest) + ) - 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(text) + nostrRepository.synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest) - if (result.firstOrNull()?.text == "EVENT") { - result.getOrNull(1)?.text?.let { subscriptionId -> - if (subscriptionId == synchronizeNostrEventRequest.id) { + 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(text) - result.getOrNull(2)?.text?.let { jsonText -> - val event = Event.fromJson( - jsonText - ) + if (result.firstOrNull()?.text == "EVENT") { + result.getOrNull(1)?.text?.let { subscriptionId -> + if (subscriptionId == synchronizeNostrEventRequest.id) { - val nostrEvent = NostrEvent( - id = event.id, - pubKey = event.pubKey, - kind = event.kind, - content = event.content, - tags = event.tags, - createdAt = Instant.fromEpochMilliseconds(event.createdAt), - sig = event.sig - ) + result.getOrNull(2)?.jsonObject?.toString()?.let { jsonText -> + logger.i("Processing: $jsonText") + val event = Event.fromJson( + jsonText + ) - nostrRepository.saveNostrEvent( - nostrEvent = nostrEvent, - synchronizeNostrEventRequest, - synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } - ) + val nostrEvent = NostrEvent( + id = event.id, + pubKey = event.pubKey, + kind = event.kind, + content = event.content, + tags = event.tags, + createdAt = Instant.fromEpochMilliseconds(event.createdAt), + unsignedNostrEventId = synchronizeNostrEventRequest.unsignedNostrEventId, + sig = event.sig + ) + nostrRepository.saveNostrEvent( + nostrEvent = nostrEvent, + synchronizeNostrEventRequest, + synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } + ) + + } } } + } else if(result.firstOrNull()?.text == "EOSE") { + result.getOrNull(1)?.text?.let { subscriptionId -> + if (subscriptionId == synchronizeNostrEventRequest.id) { + // Close the job + logger.d("We need to close the job") + } + } + } else { + logger.d("Unsupported: $text") } - } else { - logger.d("Unsupported: $text") + } catch (e: Throwable) { + logger.e("Error processing receipt: $text", e) } - } catch (e: Throwable) { - logger.e("Error processing receipt: $text", e) } + // Syncing doesn't necessary have to close after result.. + webSocketSession.close() + syncingJobs.remove(synchronizeNostrEventRequest.relayURL)?.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") } - // Syncing doesn't necessary have to close after result.. - webSocketSession.close() - syncingJobs.remove(synchronizeNostrEventRequest.relayURL)?.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") } } + + logger.i("Websocket exit") } - logger.i("Websocket exit") + syncingJobs[synchronizeNostrEventRequest.relayURL]?.join() } - - syncingJobs[synchronizeNostrEventRequest.relayURL]?.join() + } catch (e: Throwable) { + logger.e("Relay Error", e) } } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/UnqueuedProfileSynchronizationViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/UnqueuedProfileSynchronizationViewModel.kt index 69bd6ddd..eb1bc24f 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/UnqueuedProfileSynchronizationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/UnqueuedProfileSynchronizationViewModel.kt @@ -19,9 +19,11 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO +import kotlinx.coroutines.delay import kotlinx.coroutines.launch class UnqueuedProfileSynchronizationViewModel( + val unsignedNostrEventId: Long, val profilePublicKey: String, val nostrRepository: NostrRepository ): ViewModel() { @@ -29,11 +31,13 @@ class UnqueuedProfileSynchronizationViewModel( private const val TAG = "UnqueuedProfileSynchronizationViewModel" fun factory( + unsignedNostrEventId: Long, profilePublicKey: String, nostrRepository: NostrRepository ): ViewModelProvider.Factory = viewModelFactory { initializer { UnqueuedProfileSynchronizationViewModel( + unsignedNostrEventId = unsignedNostrEventId, profilePublicKey = profilePublicKey, nostrRepository = nostrRepository ) @@ -49,11 +53,14 @@ class UnqueuedProfileSynchronizationViewModel( logger.d { "queueSynchronization" } viewModelScope.launch(Dispatchers.IO) { + delay(2_000) + nostrRepository.queueSynchronizeNostrEvent( Relays.eventPublishRelaySet.map { normalizedRelayUrl -> SynchronizeNostrEventRequest( - authorPublicKeys = profilePublicKey, - kinds = "0", + authorPublicKeys = arrayOf(profilePublicKey), + unsignedNostrEventId = unsignedNostrEventId, + kinds = arrayOf(0), relayURL = normalizedRelayUrl.url ) }