Sign and queue nostrEvent

This commit is contained in:
Kgothatso Ngako
2026-03-26 22:43:32 +02:00
parent ca4af3b1ff
commit cf215da61c
14 changed files with 210 additions and 65 deletions

View File

@@ -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<String> = emptyList()
relayURLs: List<String>
) {
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
)
}

View File

@@ -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<NostrEvent?>
@Insert
suspend fun insert(nostrEvent: NostrEvent)
@Upsert
suspend fun upsert(nostrEvent: NostrEvent)
}

View File

@@ -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<PostAndProfile>
fun getAllPosts(): List<LocalPost>
@Query("SELECT * FROM Post WHERE id = :id")
fun getPostById(id: String): Flow<Post?>
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
)
}

View File

@@ -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<LocalProfile?>
}

View File

@@ -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<LocalProfile?>
@Query("SELECT * FROM UnsignedNostrEvent WHERE pubKey = :publicKey AND signedNostrEventId IS NULL")
fun observeUnsignedNostrEvents(publicKey: String): Flow<UnsignedNostrEvent>
}

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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<LocalProfile?> {
return database.profileDao().observeProfile(publicKey)
return database.unsignedNostrEventDao().observeProfile(publicKey)
}
override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent> {
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<String>) {
database.nostrDao().publishNostrEvent(
nostrEvent,
relayURLs
)
}
}

View File

@@ -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
)
}
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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<LocalProfile?>
suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent>
suspend fun createNewProfile(
publicKey: HexKey,
name: String,
@@ -23,10 +26,19 @@ interface NostrRepository {
suspend fun wipeDatabase()
suspend fun publishNostrEvent(
nostrEvent: NostrEvent,
relayURLs: List<String> = emptyList()
)
companion object {
val NO_OP_NOSTR_REPOSITORY = object : NostrRepository {
override suspend fun observeProfile(publicKey: HexKey): Flow<LocalProfile?> {
TODO("")
TODO("Not yet implemented")
}
override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent> {
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<String>) {
}
}
}
}

View File

@@ -3,5 +3,4 @@ package ac.aux.compose.repository
import ac.aux.compose.database.model.NostrEvent
interface PostRepository {
suspend fun publishPost(nostrEvent: NostrEvent)
}

View File

@@ -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<Event>(
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 {