From cf215da61c665e8b61b26b1fd17cee272edb525c Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Thu, 26 Mar 2026 22:43:32 +0200 Subject: [PATCH] Sign and queue nostrEvent --- .../ac/aux/compose/database/dao/NostrDao.kt | 87 +++++++++++++++---- .../aux/compose/database/dao/NostrEventDao.kt | 5 +- .../ac/aux/compose/database/dao/PostDao.kt | 14 +-- .../ac/aux/compose/database/dao/ProfileDao.kt | 16 +--- .../database/dao/UnsignedNostrEventDao.kt | 7 ++ .../aux/compose/database/model/NostrEvent.kt | 2 +- .../database/model/intermdiate/LocalPost.kt | 15 ++++ .../repository/DatabaseNostrRepository.kt | 14 ++- .../repository/DatabasePostRepository.kt | 10 +-- .../ac/aux/compose/managers/SeedManager.kt | 8 +- .../kotlin/ac/aux/compose/nostr/Relays.kt | 35 ++++++++ .../aux/compose/repository/NostrRepository.kt | 18 +++- .../aux/compose/repository/PostRepository.kt | 1 - .../ui/view/model/NavigationViewModel.kt | 43 ++++++++- 14 files changed, 210 insertions(+), 65 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalPost.kt create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/nostr/Relays.kt 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 fb4e46b2..66a34de7 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 @@ -9,28 +9,82 @@ import androidx.room.Dao import androidx.room.Embedded import androidx.room.Relation import androidx.room.Transaction +import co.touchlab.kermit.Logger +import kotlinx.coroutines.delay +import kotlin.time.Clock @Dao abstract class NostrDao( private val database: AuxDatabase ) { + val logger = Logger.withTag("NostrDao") @Transaction - open suspend fun insertNostrEvent( + open suspend fun publishNostrEvent( nostrEvent: NostrEvent, - relayURLs: List = emptyList() + relayURLs: List ) { - database.profileDao().insert( - Profile( - publicKey = nostrEvent.pubKey, - nostrEventId = nostrEvent.id // This is illegal... because the nostrEventId should be the one with all the profile information - ) - ) + logger.i("Publish Nostr Event: $nostrEvent ($relayURLs)") + // TODO: Find or create profile with the pubKey... if not found submit a sync request... + val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey) - // TODO: Find note that is replyTo - database.nostrEventDao().insert(nostrEvent) + 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) + + // TODO: Sync to find and update profile + } + + + database.nostrEventDao().upsert(nostrEvent) nostrEvent.toPost()?.let { post -> + if (post.replyToId != null) { + // Find or create placeHolder note that is replyTo + val replyingToPost = database.postDao().getPostById(post.replyToId) + + if (replyingToPost == null) { + // PlaceHolder and sync + database.postDao().insert( + Post( + id = post.replyToId, + nostrEventId = nostrEvent.id, // Will get overwriting by sync, + content = post.replyToId, + profilePublicKey = nostrEvent.pubKey // Will get overwriting by sync, + ) + ) + } + } + + if (post.repostId != null) { + val repostPost = database.postDao().getPostById(post.repostId) + + if (repostPost == null) { + // Placeholder Post... + database.postDao().insert( + Post( + id = post.repostId, + nostrEventId = nostrEvent.id, // Will get overwriting by sync, + content = post.repostId, + profilePublicKey = nostrEvent.pubKey // Will get overwriting by sync, + ) + ) + // Sync repostId + } + } database.postDao().insert(post) } @@ -50,6 +104,10 @@ abstract class NostrDao( database.zapDao().upsert(zap) } // Submit a broadcastRequest... + if (nostrEvent.kind == 0) { + // Look busy + delay(2_500) + } relayURLs.forEach { relayURL -> database.broadcastNostrEventRequestDao().upsert( BroadcastNostrEventRequest( @@ -59,13 +117,4 @@ abstract class NostrDao( ) } } - - data class NostrEventAndPost( - @Embedded val nostrEvent: NostrEvent, - @Relation( - parentColumn = "id", - entityColumn = "nostrEventId" - ) - val post: Post - ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt index 714e6003..504b174e 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt @@ -4,6 +4,7 @@ import ac.aux.compose.database.model.NostrEvent import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import androidx.room.Upsert import kotlinx.coroutines.flow.Flow @Dao @@ -14,6 +15,6 @@ interface NostrEventDao { @Query("SELECT * FROM NostrEvent WHERE id = :id") fun getNostrEventById(id: String): Flow - @Insert - suspend fun insert(nostrEvent: NostrEvent) + @Upsert + suspend fun upsert(nostrEvent: NostrEvent) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/PostDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/PostDao.kt index f3f9420c..516d86bd 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/PostDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/PostDao.kt @@ -2,6 +2,7 @@ package ac.aux.compose.database.dao import ac.aux.compose.database.model.Post import ac.aux.compose.database.model.Profile +import ac.aux.compose.database.model.intermdiate.LocalPost import androidx.room.Dao import androidx.room.Embedded import androidx.room.Insert @@ -12,20 +13,11 @@ import kotlinx.coroutines.flow.Flow @Dao interface PostDao { @Query("SELECT * FROM Post") - fun getAllPosts(): List + fun getAllPosts(): List @Query("SELECT * FROM Post WHERE id = :id") - fun getPostById(id: String): Flow + fun getPostById(id: String): Post? @Insert suspend fun insert(post: Post) - - data class PostAndProfile( - @Embedded val post: Post, - @Relation( - parentColumn = "profilePublicKey", - entityColumn = "publicKey" - ) - val profile: Profile - ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt index 1b0e8d01..2402dd7c 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt @@ -1,18 +1,11 @@ package ac.aux.compose.database.dao -import ac.aux.compose.database.model.NostrEvent -import ac.aux.compose.database.model.Post import ac.aux.compose.database.model.Profile -import ac.aux.compose.database.model.UnsignedNostrEvent -import ac.aux.compose.database.model.intermdiate.LocalProfile import androidx.room.Dao -import androidx.room.Embedded import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import androidx.room.Relation import androidx.room.Upsert -import kotlinx.coroutines.flow.Flow @Dao interface ProfileDao { @@ -22,15 +15,10 @@ interface ProfileDao { @Query("SELECT * FROM Profile WHERE publicKey = :publicKey") fun getProfileByPublicKey(publicKey: String): Profile? - @Insert( - onConflict = OnConflictStrategy.IGNORE - ) - suspend fun insert(profile: Profile) + @Insert + suspend fun insertPlaceholderProfile(profile: Profile) @Upsert suspend fun upsert(profile: Profile) - @Query("SELECT * FROM UnsignedNostrEvent WHERE kind = 0 AND pubKey = :publicKey") - fun observeProfile(publicKey: String): Flow - } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/UnsignedNostrEventDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/UnsignedNostrEventDao.kt index 662336fa..d83b4bdd 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/UnsignedNostrEventDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/UnsignedNostrEventDao.kt @@ -2,6 +2,7 @@ package ac.aux.compose.database.dao import ac.aux.compose.database.model.NostrEvent import ac.aux.compose.database.model.UnsignedNostrEvent +import ac.aux.compose.database.model.intermdiate.LocalProfile import androidx.room.Dao import androidx.room.Insert import androidx.room.Query @@ -21,4 +22,10 @@ interface UnsignedNostrEventDao { @Upsert suspend fun upsert(unsignedNostrEvent: UnsignedNostrEvent): Long + + @Query("SELECT * FROM UnsignedNostrEvent WHERE kind = 0 AND pubKey = :publicKey") + fun observeProfile(publicKey: String): Flow + + @Query("SELECT * FROM UnsignedNostrEvent WHERE pubKey = :publicKey AND signedNostrEventId IS NULL") + fun observeUnsignedNostrEvents(publicKey: String): Flow } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt index 6e7584cb..29c7d195 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt @@ -38,7 +38,7 @@ data class NostrEvent( val sig: HexKey, override val createdAt: Instant = Clock.System.now(), - override val updatedAt: Instant = Clock.System.now(), + override val updatedAt: Instant = createdAt, override val savedAt: Instant = Clock.System.now(), override val deletedAt: Instant? = null, override val broadcastedAt: Instant? = null, diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalPost.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalPost.kt new file mode 100644 index 00000000..269d82b3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalPost.kt @@ -0,0 +1,15 @@ +package ac.aux.compose.database.model.intermdiate + +import ac.aux.compose.database.model.Post +import ac.aux.compose.database.model.Profile +import androidx.room.Embedded +import androidx.room.Relation + +data class LocalPost( + @Embedded val post: Post, + @Relation( + parentColumn = "profilePublicKey", + entityColumn = "publicKey" + ) + val profile: Profile +) 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 0ac2409e..fc329ed5 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 @@ -1,6 +1,7 @@ package ac.aux.compose.database.repository import ac.aux.compose.database.AuxDatabase +import ac.aux.compose.database.model.NostrEvent import ac.aux.compose.database.model.Profile import ac.aux.compose.database.model.UnsignedNostrEvent import ac.aux.compose.database.model.intermdiate.LocalProfile @@ -22,7 +23,11 @@ class DatabaseNostrRepository( val logger = Logger.withTag(TAG) override suspend fun observeProfile(publicKey: HexKey): Flow { - return database.profileDao().observeProfile(publicKey) + return database.unsignedNostrEventDao().observeProfile(publicKey) + } + + override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow { + return database.unsignedNostrEventDao().observeUnsignedNostrEvents(publicKey) } override suspend fun createNewProfile( @@ -66,4 +71,11 @@ class DatabaseNostrRepository( database.wipeData() } + override suspend fun publishNostrEvent(nostrEvent: NostrEvent, relayURLs: List) { + database.nostrDao().publishNostrEvent( + nostrEvent, + relayURLs + ) + } + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabasePostRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabasePostRepository.kt index 44ac53cc..08240b7c 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabasePostRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabasePostRepository.kt @@ -7,13 +7,5 @@ import ac.aux.compose.repository.PostRepository class DatabasePostRepository( private val database: AuxDatabase ): PostRepository { - override suspend fun publishPost( - nostrEvent: NostrEvent - ) { - val nostrDao = database.nostrDao() - - nostrDao.insertNostrEvent( - nostrEvent - ) - } + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/managers/SeedManager.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/managers/SeedManager.kt index 61cad396..ee44a024 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/managers/SeedManager.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/managers/SeedManager.kt @@ -9,8 +9,12 @@ object SeedManager { privKey = Random(21_012_256L).nextBytes(32) ) - fun activePublicKey(): ByteArray { + fun activeKeyPair(): KeyPair { // TODO: Actually set and get a pair... - return tempDevelopmentKeyPair.pubKey + return tempDevelopmentKeyPair + } + + fun activePublicKey(): ByteArray { + return activeKeyPair().pubKey } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/nostr/Relays.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/nostr/Relays.kt new file mode 100644 index 00000000..f406001f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/nostr/Relays.kt @@ -0,0 +1,35 @@ +package ac.aux.compose.nostr + +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer + +object Relays { + val nos = RelayUrlNormalizer.normalize("wss://nos.lol") + val mom = RelayUrlNormalizer.normalize("wss://nostr.mom") + val primal = RelayUrlNormalizer.normalize("wss://relay.primal.net") + val damus = RelayUrlNormalizer.normalize("wss://relay.damus.io") + val wine = RelayUrlNormalizer.normalize("wss://nostr.wine") + + val where = RelayUrlNormalizer.normalize("wss://relay.noswhere.com") + val elites = RelayUrlNormalizer.normalize("wss://nostrelites.org") + + val bitcoiner = RelayUrlNormalizer.normalize("wss://nostr.bitcoiner.social") + val oxtr = RelayUrlNormalizer.normalize("wss://nostr.oxtr.dev") + + val nostoday = RelayUrlNormalizer.normalize("wss://search.nos.today") + val antiprimal = RelayUrlNormalizer.normalize("wss://antiprimal.net") + val ditto = RelayUrlNormalizer.normalize("wss://relay.ditto.pub") + + val auth = RelayUrlNormalizer.normalize("wss://auth.nostr1.com") + val oxchat = RelayUrlNormalizer.normalize("wss://relay.0xchat.com") + + val news = RelayUrlNormalizer.normalize("wss://news.utxo.one") + + val purplepages = RelayUrlNormalizer.normalize("wss://purplepag.es") + val coracle = RelayUrlNormalizer.normalize("wss://indexer.coracle.social") + val userkinds = RelayUrlNormalizer.normalize("wss://user.kindpag.es") + val yabu = RelayUrlNormalizer.normalize("wss://directory.yabu.me") + val nostr1 = RelayUrlNormalizer.normalize("wss://profiles.nostr1.com") + + val bootstrapInboxRelaySet = setOf(damus, primal, mom, nos, bitcoiner, oxtr, yabu) + val eventFinderRelaySet = setOf(wine, damus, primal, mom, nos, bitcoiner, oxtr) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt index 3843d28a..ca491c2e 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt @@ -1,5 +1,6 @@ package ac.aux.compose.repository +import ac.aux.compose.database.model.NostrEvent import ac.aux.compose.database.model.Profile import ac.aux.compose.database.model.UnsignedNostrEvent import ac.aux.compose.database.model.intermdiate.LocalProfile @@ -9,6 +10,8 @@ import kotlinx.coroutines.flow.Flow interface NostrRepository { suspend fun observeProfile(publicKey: HexKey): Flow + suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow + suspend fun createNewProfile( publicKey: HexKey, name: String, @@ -23,10 +26,19 @@ interface NostrRepository { suspend fun wipeDatabase() + suspend fun publishNostrEvent( + nostrEvent: NostrEvent, + relayURLs: List = emptyList() + ) + companion object { val NO_OP_NOSTR_REPOSITORY = object : NostrRepository { override suspend fun observeProfile(publicKey: HexKey): Flow { - TODO("") + TODO("Not yet implemented") + } + + override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow { + TODO("Not yet implemented") } override suspend fun createNewProfile( @@ -44,6 +56,10 @@ interface NostrRepository { override suspend fun wipeDatabase() { } + + override suspend fun publishNostrEvent(nostrEvent: NostrEvent, relayURLs: List) { + + } } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/PostRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/PostRepository.kt index 11f3998d..70516ea8 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/PostRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/PostRepository.kt @@ -3,5 +3,4 @@ package ac.aux.compose.repository import ac.aux.compose.database.model.NostrEvent interface PostRepository { - suspend fun publishPost(nostrEvent: NostrEvent) } \ 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 e015ff9f..4ff6cfb9 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 @@ -1,6 +1,8 @@ package ac.aux.compose.ui.view.model +import ac.aux.compose.database.model.NostrEvent import ac.aux.compose.managers.SeedManager +import ac.aux.compose.nostr.Relays import ac.aux.compose.repository.NostrRepository import ac.aux.compose.ui.view.state.NavigationUIState import androidx.lifecycle.ViewModel @@ -9,16 +11,15 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.getAndUpdate -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlin.time.Instant class NavigationViewModel( initialNavigationUIState: NavigationUIState, @@ -48,12 +49,46 @@ class NavigationViewModel( val navigationUIState = _navigationUIState.asStateFlow() init { + observeUnsignedNostrEvents() observeProfile() } + private fun observeUnsignedNostrEvents() { + val tempSigner = NostrSignerSync( + SeedManager.activeKeyPair() + ) + logger.i { "observeUnsignedNostrEvents" } + viewModelScope.launch { + nostrRepository.observeUnsignedNostrEvents( + publicKey = SeedManager.activePublicKey().toHexKey() + ).collect { unsignedNostrEvent -> + + val event = tempSigner.signNormal( + createdAt = unsignedNostrEvent.createdAt.toEpochMilliseconds(), + kind = unsignedNostrEvent.kind, + tags = unsignedNostrEvent.tags, + content = unsignedNostrEvent.content + ) + + nostrRepository.publishNostrEvent( + NostrEvent( + id = event.id, + pubKey = event.pubKey, + kind = event.kind, + tags = event.tags, + content = event.content, + createdAt = Instant.fromEpochMilliseconds(event.createdAt), + sig = event.sig + ), + relayURLs = Relays.eventFinderRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } + ) + + } + } + } private fun observeProfile() { logger.i("observeProfile") viewModelScope.launch {