chore: merge the develop branch into master #1

Merged
reya merged 43 commits from develop into master 2026-05-23 00:50:13 +00:00
3 changed files with 126 additions and 48 deletions
Showing only changes of commit 3376e71bda - Show all commits

View File

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

View File

@@ -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
when (notification) {
is ClientNotification.Message -> {
// TODO: Handle message
}
override suspend fun handleMsg(
relayUrl: RelayUrl,
msg: RelayMessage
) {
TODO("Not yet implemented")
is ClientNotification.NewEvent -> {
// TODO: Handle new event
}
})
is ClientNotification.Shutdown -> {
break
}
}
}
}
suspend fun getDefaultRelayList(): Map<RelayUrl, RelayMetadata> {
// Construct a list of relays
val relayList = mapOf<RelayUrl, RelayMetadata>(
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<RelayUrl> {
// 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)
}
}

View File

@@ -24,11 +24,11 @@ class NostrViewModel(
val isCreating = _isCreating.asStateFlow()
fun initAndConnect(dbPath: String) {
viewModelScope.launch {
try {
// Initialize nostr client
nostr.init(dbPath)
viewModelScope.launch {
try {
// Connect to bootstrap relays
nostr.connect()