diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index c410d30..752c2c6 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -26,7 +26,7 @@ kotlin { commonMain.dependencies { implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") - implementation("org.rust-nostr:nostr-sdk-kmp:0.44.3") + implementation("su.reya:nostr-sdk-kmp:0.1") } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt index 0dd2978..80c3cfc 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt @@ -2,11 +2,11 @@ package su.reya.coop import rust.nostr.sdk.Client import rust.nostr.sdk.ClientBuilder -import rust.nostr.sdk.ClientOptions -import rust.nostr.sdk.Event +import rust.nostr.sdk.ClientNotification +import rust.nostr.sdk.Contact import rust.nostr.sdk.EventBuilder import rust.nostr.sdk.Filter -import rust.nostr.sdk.HandleNotification +import rust.nostr.sdk.GossipConfig import rust.nostr.sdk.Keys import rust.nostr.sdk.Kind import rust.nostr.sdk.KindStandard @@ -16,9 +16,12 @@ import rust.nostr.sdk.NostrConnect import rust.nostr.sdk.NostrDatabase import rust.nostr.sdk.NostrGossip import rust.nostr.sdk.NostrSigner -import rust.nostr.sdk.RelayMessage +import rust.nostr.sdk.PublicKey +import rust.nostr.sdk.RelayCapabilities +import rust.nostr.sdk.RelayMetadata import rust.nostr.sdk.RelayUrl import rust.nostr.sdk.ReqExitPolicy +import rust.nostr.sdk.ReqTarget import rust.nostr.sdk.SubscribeAutoCloseOptions import rust.nostr.sdk.Timestamp @@ -28,39 +31,59 @@ class Nostr { var signer: NostrSigner? = null private set - fun init(dbPath: String) { + suspend fun init(dbPath: String) { val lmdb = NostrDatabase.lmdb(dbPath) val gossip = NostrGossip.inMemory() - val opts = ClientOptions().automaticAuthentication(false) - client = ClientBuilder().database(lmdb).gossip(gossip).opts(opts).build() + client = + ClientBuilder() + .database(lmdb) + .gossip(gossip) + .gossipConfig(GossipConfig().noBackgroundRefresh()) + .maxRelays(20u) + .verifySubscriptions(false) + .automaticAuthentication(false) + .build() } suspend fun connect() { - this.client?.addRelay(RelayUrl.parse("wss://relay.damus.io")) - this.client?.addRelay(RelayUrl.parse("wss://relay.primal.net")) - this.client?.addRelay(RelayUrl.parse("wss://user.kindpag.es")) - this.client?.connect() + client?.addRelay( + url = RelayUrl.parse("wss://relay.damus.io"), + capabilities = RelayCapabilities.none() + ) + client?.addRelay( + url = RelayUrl.parse("wss://relay.primal.net"), + capabilities = RelayCapabilities.none() + ) + client?.addRelay( + url = RelayUrl.parse("wss://user.kindpag.es"), + capabilities = RelayCapabilities.none() + ) + client?.addRelay( + url = RelayUrl.parse("https://indexer.coracle.social"), + capabilities = RelayCapabilities.gossip() + ) + client?.connect() } suspend fun disconnect() { - this.client?.shutdown() + client?.shutdown() } suspend fun setKeySigner(keys: Keys) { signer = NostrSigner.keys(keys) - this.getMetadata() + getUserMetadata() } - suspend fun setRemoteSigner(signer: NostrConnect) { - this.signer = NostrSigner.nostrConnect(signer) - this.getMetadata() + suspend fun setRemoteSigner(remote: NostrConnect) { + signer = NostrSigner.nostrConnect(remote) + getUserMetadata() } - suspend fun getMetadata() { - val currentUserPubKey = this.signer?.getPublicKey() ?: return - val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose) - val filter = Filter().author(currentUserPubKey).limit(10u).kinds( + suspend fun getUserMetadata() { + val userPubkey = signer?.getPublicKey() ?: return + + val filter = Filter().author(userPubkey).limit(10u).kinds( listOf( Kind.fromStd(KindStandard.METADATA), Kind.fromStd(KindStandard.CONTACT_LIST), @@ -68,44 +91,99 @@ class Nostr { ) ) - this.client?.subscribe(filter, opts) + val target = ReqTarget.auto(listOf(filter)) + val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose) + + client?.subscribe(target = target, id = "user-metadata", closeOn = opts) } suspend fun handleNotifications() { val now = Timestamp.now() + val notifications = client?.notifications() - this.client?.handleNotifications(object : HandleNotification { - override suspend fun handle(relayUrl: RelayUrl, subscriptionId: String, event: Event) { - TODO("Not yet implemented") - } + while (true) { + val notification = notifications?.next() ?: break - override suspend fun handleMsg( - relayUrl: RelayUrl, - msg: RelayMessage - ) { - TODO("Not yet implemented") + when (notification) { + is ClientNotification.Message -> { + // TODO: Handle message + } + + is ClientNotification.NewEvent -> { + // TODO: Handle new event + } + + is ClientNotification.Shutdown -> { + break + } } - }) + } + } + + suspend fun getDefaultRelayList(): Map { + // Construct a list of relays + val relayList = mapOf( + RelayUrl.parse("wss://relay.damus.io") to RelayMetadata.READ, + RelayUrl.parse("wss://relay.primal.net") to RelayMetadata.READ, + RelayUrl.parse("wss://relay.nostr.net") to RelayMetadata.WRITE, + RelayUrl.parse("wss://nostr.superfriends.online") to RelayMetadata.WRITE + ) + + // Ensure all relays are added and connected + relayList.forEach { (relay, metadata) -> + client?.addRelay( + url = relay, + capabilities = + if (metadata == RelayMetadata.READ) RelayCapabilities.read() + else if (metadata == RelayMetadata.WRITE) RelayCapabilities.write() + else RelayCapabilities.none() + ) + client?.connectRelay(relay) + } + + return relayList + } + + suspend fun getMsgRelayList(): List { + // Construct a list of messaging relays + val msgRelayList = listOf( + RelayUrl.parse("wss://relay.0xchat.com"), + RelayUrl.parse("wss://nip17.com"), + ) + + // Ensure all relays are added and connected + msgRelayList.forEach { relay -> + client?.addRelay(relay, RelayCapabilities.none()) + client?.connectRelay(relay) + } + + return msgRelayList } suspend fun createIdentity(keys: Keys, name: String, bio: String, picture: String?) { // Set signer signer = NostrSigner.keys(keys) - // Construct metadata records - val records = MetadataRecord( - name = name, - displayName = name, - about = bio, - picture = picture - ) + // Send relay list event + val relayList = getDefaultRelayList() + val relayListEvent = EventBuilder.relayList(relayList).sign(signer!!); + client?.sendEvent(relayListEvent) - // Construct a nostr event and sign it - val metadata = Metadata.fromRecord(records) - val builder = EventBuilder.metadata(metadata).build(keys.publicKey()) - val event = this.signer?.signEvent(builder) ?: return + // Send messaging relay list event + val msgRelayList = getMsgRelayList() + val msgRelayListEvent = EventBuilder.nip17RelayList(msgRelayList).sign(signer!!) + client?.sendEventNoWait(msgRelayListEvent) - // Send event to relays - this.client?.sendEvent(event) + // Send metadata event + val metadata = + Metadata.fromRecord(MetadataRecord(name = name, about = bio, picture = picture)) + val metadataEvent = EventBuilder.metadata(metadata).sign(signer!!) + client?.sendEventNoWait(metadataEvent) + + // Send contact list event + val defaultContact = + listOf(Contact(publicKey = PublicKey.parse("npub1j3rz3ndl902lya6ywxvy5c983lxs8mpukqnx4pa4lt5wrykwl5ys7wpw3x"))) + val contactListEvent = EventBuilder.contactList(defaultContact).sign(signer!!) + client?.sendEventNoWait(contactListEvent) } } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt index d83a7ed..ff50dff 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt @@ -24,11 +24,11 @@ class NostrViewModel( val isCreating = _isCreating.asStateFlow() fun initAndConnect(dbPath: String) { - // Initialize nostr client - nostr.init(dbPath) - viewModelScope.launch { try { + // Initialize nostr client + nostr.init(dbPath) + // Connect to bootstrap relays nostr.connect()