simple nostr service

This commit is contained in:
2026-04-28 09:35:33 +07:00
parent feffda519f
commit 76050b5410
3 changed files with 129 additions and 29 deletions

View File

@@ -14,7 +14,6 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.coroutines.flow.flow
import su.reya.coop.coop.storage.SecretStore
import su.reya.coop.screens.ChatScreen
import su.reya.coop.screens.HomeScreen
@@ -37,19 +36,23 @@ fun App(dbPath: String) {
MaterialExpressiveTheme {
rememberCoroutineScope()
val navController = rememberNavController()
val hasSecret by viewModel.hasSecret.collectAsState(initial = null)
// Get user's signer status
val hasSecretFlow = remember {
flow {
emit(secretStore.has("user_signer"))
}
}
val hasSecret by hasSecretFlow.collectAsState(initial = null)
LaunchedEffect(hasSecret) {
// Navigate to the home screen if the secret is already set
if (hasSecret == true) {
// Start a background notification handler
viewModel.startNotificationHandler()
if (hasSecret == null) {
// Loading state
return@MaterialExpressiveTheme
// Navigate to the home screen
navController.navigate(Screen.Home) {
popUpTo(Screen.Onboarding) { inclusive = true }
}
}
}
// Show loading screen while initializing
if (hasSecret == null) return@MaterialExpressiveTheme
NavHost(
navController = navController,
@@ -71,7 +74,6 @@ fun App(dbPath: String) {
isLoading = isCreating,
onSave = { name, bio, uri ->
viewModel.createIdentity(name, bio, uri?.toString())
navController.navigate(Screen.Home)
}
)
}

View File

@@ -3,22 +3,30 @@ 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.EventBuilder
import rust.nostr.sdk.Filter
import rust.nostr.sdk.HandleNotification
import rust.nostr.sdk.Keys
import rust.nostr.sdk.Kind
import rust.nostr.sdk.KindStandard
import rust.nostr.sdk.Metadata
import rust.nostr.sdk.MetadataRecord
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.RelayUrl
import rust.nostr.sdk.ReqExitPolicy
import rust.nostr.sdk.SubscribeAutoCloseOptions
import rust.nostr.sdk.Timestamp
class Nostr {
var client: Client? = null
private set
var signer: NostrSigner? = null
private set
var deviceSigner: NostrSigner? = null
private set
fun init(dbPath: String) {
val lmdb = NostrDatabase.lmdb(dbPath)
@@ -39,20 +47,61 @@ class Nostr {
this.client?.shutdown()
}
suspend fun setKeySigner(keys: Keys) {
signer = NostrSigner.keys(keys)
this.getMetadata()
}
suspend fun setRemoteSigner(signer: NostrConnect) {
this.signer = NostrSigner.nostrConnect(signer)
this.getMetadata()
}
suspend fun getMetadata() {
val currentUserPubKey = this.signer?.getPublicKey() ?: return
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
val filter = Filter().author(currentUserPubKey).limit(10u).kinds(
listOf(
Kind.fromStd(KindStandard.METADATA),
Kind.fromStd(KindStandard.CONTACT_LIST),
Kind.fromStd(KindStandard.INBOX_RELAYS)
)
)
this.client?.subscribe(filter, opts)
}
suspend fun handleNotifications() {
val now = Timestamp.now()
this.client?.handleNotifications(object : HandleNotification {
override suspend fun handle(relayUrl: RelayUrl, subscriptionId: String, event: Event) {
TODO("Not yet implemented")
}
override suspend fun handleMsg(
relayUrl: RelayUrl,
msg: RelayMessage
) {
TODO("Not yet implemented")
}
})
}
suspend fun createIdentity(keys: Keys, name: String, bio: String, picture: String?) {
// Set signer
signer = NostrSigner.keys(keys)
// Construct metadata
val metadata = Metadata.fromRecord(
MetadataRecord(
// Construct metadata records
val records = MetadataRecord(
name = name,
displayName = name,
about = bio,
picture = picture
)
)
// Construct event and sign it
// 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

View File

@@ -8,14 +8,17 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import rust.nostr.sdk.Keys
import rust.nostr.sdk.NostrConnect
import rust.nostr.sdk.NostrConnectUri
import su.reya.coop.storage.SecretStorage
import kotlin.time.Duration
class NostrViewModel(
private val nostr: Nostr,
private val secretStore: SecretStorage
) : ViewModel() {
private val _isConnected = MutableStateFlow(false)
val isConnected = _isConnected.asStateFlow()
private val _hasSecret = MutableStateFlow<Boolean?>(null)
val hasSecret = _hasSecret.asStateFlow()
private val _isCreating = MutableStateFlow(false)
val isCreating = _isCreating.asStateFlow()
@@ -28,14 +31,61 @@ class NostrViewModel(
try {
// Connect to bootstrap relays
nostr.connect()
_isConnected.value = true
// Get user's signer secret
val secret = secretStore.get("user_signer")
// If no secret is found, show onboarding screen
if (secret == null) {
_hasSecret.value = false
return@launch
}
_hasSecret.value = true
// Handle different signer types
if (secret.startsWith("nsec1")) {
val keys = Keys.parse(secret)
nostr.setKeySigner(keys)
} else if (secret.startsWith("bunker://")) {
val appKeys = getOrInitAppKeys()
val bunker = NostrConnectUri.parse(secret)
val remote = NostrConnect(
uri = bunker,
appKeys = appKeys,
timeout = Duration.parse("5"),
opts = null
)
nostr.setRemoteSigner(remote)
} else {
throw IllegalArgumentException("Invalid secret format: $secret")
}
} catch (e: Exception) {
_isConnected.value = false
println(e)
println("Failed to connect: ${e.message}")
}
}
}
fun startNotificationHandler() {
viewModelScope.launch {
nostr.handleNotifications()
}
}
suspend fun getOrInitAppKeys(): Keys {
val secret = secretStore.get("app_keys")
// If app keys are already stored, use them
if (secret != null) {
return Keys.parse(secret)
}
// Generate new app keys and save to the secret storage
val keys = Keys.generate()
secretStore.set("app_keys", keys.secretKey().toBech32())
return keys
}
fun createIdentity(name: String, bio: String, picture: String?) {
viewModelScope.launch {
try {
@@ -48,8 +98,7 @@ class NostrViewModel(
// Save secret to the secret storage
secretStore.set("user_signer", secret)
} catch (e: Exception) {
_isCreating.value = false
println(e)
println("Create identity failed: $e")
}
}
}