simple nostr service
This commit is contained in:
@@ -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"))
|
||||
LaunchedEffect(hasSecret) {
|
||||
// Navigate to the home screen if the secret is already set
|
||||
if (hasSecret == true) {
|
||||
// Start a background notification handler
|
||||
viewModel.startNotificationHandler()
|
||||
|
||||
// Navigate to the home screen
|
||||
navController.navigate(Screen.Home) {
|
||||
popUpTo(Screen.Onboarding) { inclusive = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
val hasSecret by hasSecretFlow.collectAsState(initial = null)
|
||||
|
||||
if (hasSecret == null) {
|
||||
// Loading state
|
||||
return@MaterialExpressiveTheme
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 createIdentity(keys: Keys, name: String, bio: String, picture: String?) {
|
||||
suspend fun setKeySigner(keys: Keys) {
|
||||
signer = NostrSigner.keys(keys)
|
||||
this.getMetadata()
|
||||
}
|
||||
|
||||
// Construct metadata
|
||||
val metadata = Metadata.fromRecord(
|
||||
MetadataRecord(
|
||||
name = name,
|
||||
displayName = name,
|
||||
about = bio,
|
||||
picture = picture
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
// Construct event and sign it
|
||||
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 records
|
||||
val records = MetadataRecord(
|
||||
name = name,
|
||||
displayName = name,
|
||||
about = bio,
|
||||
picture = picture
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user