From 31a20122d321853fa179e4c5fe36966d038759a4 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 18 Jun 2026 08:57:38 +0700 Subject: [PATCH] add verify steps --- .../su/reya/coop/screens/ProfileScreen.kt | 26 ++++++++++++++-- .../commonMain/kotlin/su/reya/coop/Nostr.kt | 30 +++++++++++++++++++ .../kotlin/su/reya/coop/NostrViewModel.kt | 19 ++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ProfileScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ProfileScreen.kt index c2b7f6a..d635d80 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ProfileScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ProfileScreen.kt @@ -27,16 +27,19 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.toShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_chat @@ -44,6 +47,7 @@ import coop.composeapp.generated.resources.ic_share import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource import rust.nostr.sdk.PublicKey +import rust.nostr.sdk.Timestamp import su.reya.coop.LocalNavigator import su.reya.coop.LocalNostrViewModel import su.reya.coop.LocalSnackbarHostState @@ -64,9 +68,9 @@ fun ProfileScreen(pubkey: String, screening: Boolean = false) { val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return val metadataFlow = remember(pubkey) { viewModel.getMetadata(pubkey) } - val metadata by metadataFlow.collectAsState(initial = null) - val profile = metadata?.asRecord() + val metadata by metadataFlow.collectAsStateWithLifecycle() + val profile = metadata?.asRecord() val displayName = profile?.displayName ?: profile?.name ?: "No name" val nip05 = profile?.nip05 ?: pubkey.short() val picture = profile?.picture @@ -79,6 +83,22 @@ fun ProfileScreen(pubkey: String, screening: Boolean = false) { ) } + var addressVerified by remember { mutableStateOf(false) } + var lastActivity by remember { mutableStateOf(null) } + + LaunchedEffect(screening) { + if (screening) { + scope.launch { + // Verify NIP-05 address if present + profile?.nip05?.let { it -> + viewModel.verifyAddress(pubkey, it).let { addressVerified = it } + } + // Get the last activity + viewModel.verifyActivity(pubkey)?.let { lastActivity = it } + } + } + } + Scaffold( containerColor = MaterialTheme.colorScheme.surfaceContainer, snackbarHost = { SnackbarHost(snackbarHostState) }, diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt index 5baa07e..66e70e6 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt @@ -947,4 +947,34 @@ class Nostr { throw IllegalStateException("Failed to search nostr: ${e.message}", e) } } + + suspend fun verifyAddress(pubkey: PublicKey, address: String): Boolean { + try { + val address = Nip05Address.parse(address) + val profile = profileFromAddress(HttpClient(), address) + + return profile.publicKey() == pubkey + } catch (e: Exception) { + throw IllegalStateException("Failed to verify address: ${e.message}", e) + } + } + + suspend fun verifyActivity(pubkey: PublicKey): Timestamp? { + try { + val filter = Filter().author(pubkey).limit(3u) + val target = mutableMapOf>() + NostrManager.BOOTSTRAP_RELAYS.forEach { relay -> + target[RelayUrl.parse(relay)] = listOf(filter) + } + + val events = client?.fetchEvents( + target = ReqTarget.manual(target), + timeout = Duration.parse("3s") + ) + + return events?.first()?.createdAt() + } catch (e: Exception) { + throw IllegalStateException("Failed to get latest activity: ${e.message}", e) + } + } } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt index b324b9c..5544d9c 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt @@ -35,6 +35,7 @@ import rust.nostr.sdk.PublicKey import rust.nostr.sdk.RelayMetadata import rust.nostr.sdk.RelayUrl import rust.nostr.sdk.Tag +import rust.nostr.sdk.Timestamp import rust.nostr.sdk.UnsignedEvent import su.reya.coop.blossom.BlossomClient import su.reya.coop.storage.SecretStorage @@ -820,6 +821,24 @@ class NostrViewModel( } return emptyList() } + + suspend fun verifyAddress(pubkey: PublicKey, address: String): Boolean { + return try { + nostr.verifyAddress(pubkey, address) + } catch (e: Exception) { + showError("Error: ${e.message}") + false + } + } + + suspend fun verifyActivity(pubkey: PublicKey): Timestamp? { + return try { + nostr.verifyActivity(pubkey) + } catch (e: Exception) { + showError("Error: ${e.message}") + null + } + } } fun PublicKey.short(): String {