package su.reya.coop.viewmodel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import rust.nostr.sdk.AsyncNostrSigner import rust.nostr.sdk.EncryptedSecretKey import rust.nostr.sdk.Keys import rust.nostr.sdk.NostrConnect import rust.nostr.sdk.NostrConnectUri import rust.nostr.sdk.PublicKey import su.reya.coop.nostr.ExternalSignerHandler import su.reya.coop.nostr.ExternalSignerProxy import su.reya.coop.nostr.Nostr import su.reya.coop.nostr.SignerPermissions import su.reya.coop.repository.MediaRepository import su.reya.coop.storage.SecretStorage import kotlin.time.Duration.Companion.seconds data class AuthState( val signerRequired: Boolean? = null, val isNotificationBannerDismissed: Boolean = false, ) class AuthViewModel( private val nostr: Nostr, private val secretStore: SecretStorage, private val externalSignerHandler: ExternalSignerHandler? = null, ) : BaseViewModel() { private val mediaRepository = MediaRepository() companion object { private const val KEY_USER_SIGNER = "user_signer" private const val KEY_APP_KEYS = "app_keys" private const val KEY_BANNER_DISMISSED = "notification_banner_dismissed" } private val _state = MutableStateFlow(AuthState()) val state = _state.asStateFlow() init { // Check if the notification banner has been dismissed checkNotificationBannerDismissedStatus() // Check local stored secret (secret key or bunker) login() } private fun checkNotificationBannerDismissedStatus() { viewModelScope.launch { val dismissed = secretStore.get(KEY_BANNER_DISMISSED) == "true" _state.update { it.copy(isNotificationBannerDismissed = dismissed) } } } private fun login() { viewModelScope.launch { try { val secret = withTimeoutOrNull(5.seconds) { secretStore.get(KEY_USER_SIGNER) } if (secret == null) { _state.update { it.copy(signerRequired = true) } return@launch } runCatching { val (signer, _) = createSigner(secret) nostr.setSigner(signer) }.onSuccess { _state.update { it.copy(signerRequired = false) } }.onFailure { e -> showError("Login failed: ${e.message}") _state.update { it.copy(signerRequired = true) } } } catch (e: Exception) { showError("Login failed: ${e.message}") _state.update { it.copy(signerRequired = true) } } } } fun logout(onLogout: () -> Unit = {}) { viewModelScope.launch { try { // Reset the nostr signer and prune the database nostr.signer.switch(Keys.generate()) nostr.prune() } catch (e: Exception) { showError("Logout encountered an error: ${e.message}") } finally { // Clear credentials from persistent storage secretStore.clear(KEY_USER_SIGNER) secretStore.clear(KEY_BANNER_DISMISSED) // Call cleanup callback (e.g. to reset other ViewModels) onLogout() // Reset local states _state.update { it.copy(signerRequired = true) } } } } fun dismissNotificationBanner() { viewModelScope.launch { secretStore.set(KEY_BANNER_DISMISSED, "true") _state.update { it.copy(isNotificationBannerDismissed = true) } } } private suspend fun getOrInitAppKeys(): Keys { val secret = secretStore.get(KEY_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(KEY_APP_KEYS, keys.secretKey().toBech32()) return keys } private suspend fun createSigner( secret: String, password: String? = null ): Pair { return when { secret.startsWith("nsec1") -> Keys.parse(secret) to null secret.startsWith("ncryptsec1") -> { if (password == null) throw IllegalArgumentException("Password is required") val enc = EncryptedSecretKey.fromBech32(secret) val secret = enc.decrypt(password) val keys = Keys(secret) keys to keys.secretKey().toBech32() } secret.startsWith("bunker://") -> { val appKeys = getOrInitAppKeys() val bunker = NostrConnectUri.parse(secret) val timeout = 50.seconds NostrConnect(uri = bunker, appKeys, timeout, null) to null } secret.startsWith("nip55://") -> { val handler = externalSignerHandler ?: throw IllegalStateException("External signer not available on this platform") // Format: nip55://packageName/hexPubkey val parts = secret.removePrefix("nip55://").split("/", limit = 2) val packageName = parts[0] val pubkey = PublicKey.parse(parts[1]) handler.setPackageName(packageName) ExternalSignerProxy(handler, pubkey) to null } else -> throw IllegalArgumentException("Invalid secret format") } } suspend fun importIdentity(secret: String, password: String? = null) { val (signer, decryptedSecret) = createSigner(secret, password) // Update signer nostr.setSigner(signer) // Persist the secret in the secret storage secretStore.set(KEY_USER_SIGNER, decryptedSecret ?: secret) // Update local states _state.update { it.copy(signerRequired = false) } } suspend fun connectExternalSigner() { val handler = externalSignerHandler ?: throw IllegalStateException("Signer not available") val permissions = SignerPermissions.toJson( listOf( SignerPermissions.signEvent(0), SignerPermissions.signEvent(3), SignerPermissions.signEvent(10000), SignerPermissions.signEvent(10050), SignerPermissions.signEvent(10063), SignerPermissions.signEvent(22242), SignerPermissions.signEvent(30030), SignerPermissions.signEvent(30315), SignerPermissions.nip44Encrypt(), SignerPermissions.nip44Decrypt(), ) ) val result = handler.getPublicKey(permissions) ?: throw Exception("Rejected") val signer = ExternalSignerProxy(handler, result.pubkey) // Update signer nostr.setSigner(signer) // Store the signer in the secret storage secretStore.set(KEY_USER_SIGNER, "nip55://${result.packageName}/${result.pubkey.toHex()}") // Update local states _state.update { it.copy(signerRequired = false) } } fun isExternalSignerAvailable(): Boolean { return externalSignerHandler?.isAvailable() == true } suspend fun createIdentity( name: String, bio: String?, picture: ByteArray?, contentType: String? = null ) { val keys = Keys.generate() val secret = keys.secretKey().toBech32() val avatarUrl = picture?.let { mediaRepository.blossomUpload(keys, it, contentType ?: "image/jpeg") } // Create identity nostr.profiles.createIdentity(keys = keys, name = name, bio = bio, picture = avatarUrl) // Persist the secret in the secret storage secretStore.set(KEY_USER_SIGNER, secret) // Update local states _state.update { it.copy(signerRequired = false) } } }